28

我们有一个异步任务,它为对象执行可能长时间运行的计算。然后将结果缓存在对象上。为了防止多个任务重复相同的工作,我们通过原子 SQL 更新添加了锁定:

UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0

锁定仅适用于异步任务。对象本身仍可能由用户更新。如果发生这种情况,旧版本对象的任何未完成任务都应丢弃其结果,因为它们可能已过时。使用原子 SQL 更新也很容易做到这一点:

UPDATE objects SET results = '...' WHERE id = 1234 AND version = 1

如果对象已更新,则其版本将不匹配,因此将丢弃结果。

这两个原子更新应该处理任何可能的竞争条件。问题是如何在单元测试中验证这一点。

第一个信号量很容易测试,因为它只是设置两个不同的测试,有两种可能的场景:(1)对象被锁定的地方和(2)对象没有被锁定的地方。(我们不需要测试 SQL 查询的原子性,因为这应该是数据库供应商的责任。)

如何测试第二个信号量?在第一个信号量之后但在第二个信号量之前的某个时间,第三方需要更改对象。这将需要暂停执行,以便可以可靠且一致地执行更新,但我知道不支持使用 RSpec 注入断点。有没有办法做到这一点?还是有一些我忽略的其他技术来模拟这种竞争条件?

4

1 回答 1

28

您可以从电子制造中借鉴一个想法,并将测试挂钩直接放入生产代码中。就像电路板可以制造有专门的地方供测试设备控制和探测电路一样,我们也可以用代码做同样的事情。

假设我们有一些代码在数据库中插入一行:

class TestSubject

  def insert_unless_exists
    if !row_exists?
      insert_row
    end
  end

end

但是此代码在多台计算机上运行。那么就有一个竞争条件,因为另一个进程可能会在我们的测试和我们的插入之间插入行,从而导致 DuplicateKey 异常。我们想测试我们的代码是否处理了由该竞争条件导致的异常。为了做到这一点,我们的测试需要在调用 to 之后、调用 torow_exists?之前插入行insert_row。所以让我们在那里添加一个测试钩子:

class TestSubject

  def insert_unless_exists
    if !row_exists?
      before_insert_row_hook
      insert_row
    end
  end

  def before_insert_row_hook
  end

end

当在野外运行时,钩子除了消耗一点点 CPU 时间外什么都不做。但是当测试代码的竞争条件时,测试猴子补丁 before_insert_row_hook:

class TestSubject
  def before_insert_row_hook
    insert_row
  end
end

这不是狡猾吗?就像寄生蜂幼虫劫持了毫无防备的毛毛虫的身体一样,测试劫持了被测代码,以便它创建我们需要测试的确切条件。

这个想法就像 XOR 游标一样简单,所以我怀疑很多程序员已经独立发明了它。我发现它对于测试具有竞争条件的代码通常很有用。我希望它有所帮助。

于 2010-01-10T05:30:15.673 回答