在RSpec单元测试中模拟竞争条件

时间:2010-01-07 01:20:32

标签: ruby-on-rails ruby unit-testing rspec race-condition

我们有一个异步任务,可以为对象执行可能长时间运行的计算。然后将结果缓存在对象上。为了防止多个任务重复相同的工作,我们使用原子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注入断点。有没有办法做到这一点?还是有一些其他技术我忽略了模拟这样的竞争条件?

1 个答案:

答案 0 :(得分:27)

您可以从电子制造中借鉴一个想法,并将测试挂钩直接放入生产代码中。正如电路板可以制造出具有特殊位置的测试设备来控制和探测电路一样,我们可以用代码做同样的事情。

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

class TestSubject

  def insert_unless_exists
    if !row_exists?
      insert_row
    end
  end

end

但是这段代码在多台计算机上运行。然后,有一个竞争条件,因为另一个进程可能在我们的测试和插入之间插入行,从而导致DuplicateKey异常。我们想测试我们的代码处理由该竞争条件导致的异常。为了做到这一点,我们的测试需要在调用row_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游标一样简单,所以我怀疑很多程序员已经独立发明了它。我发现它通常用于测试具有竞争条件的代码。我希望它有所帮助。