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

Ian*_*nce 27 ruby unit-testing rspec ruby-on-rails race-condition

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

UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0
Run Code Online (Sandbox Code Playgroud)

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

UPDATE objects SET results = '...' WHERE id = 1234 AND version = 1
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Way*_*rad 27

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

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

class TestSubject

  def insert_unless_exists
    if !row_exists?
      insert_row
    end
  end

end
Run Code Online (Sandbox Code Playgroud)

但是这段代码在多台计算机上运行.然后,有一个竞争条件,因为另一个进程可能在我们的测试和插入之间插入行,从而导致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
Run Code Online (Sandbox Code Playgroud)

当在野外运行时,除了占用一点CPU时间之外,钩子什么都不做.但是当代码正在针对竞争条件进行测试时,测试猴子补丁before_insert_row_hook:

class TestSubject
  def before_insert_row_hook
    insert_row
  end
end
Run Code Online (Sandbox Code Playgroud)

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

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

  • 在您的明喻中使用寄生蜂黄虫的+1. (4认同)
  • @WayneConrad 我不确定你是邪恶天才还是普通天才,但这就是……天才。谢谢,被盗了! (2认同)
  • @JoeCairns 我很高兴成为任何一种天才。谢谢你的夸奖,我很高兴这个想法对你有帮助。 (2认同)