模拟 RSpec 单元测试中的竞争条件
我们有一个异步任务,它对对象执行可能需要长时间运行的计算。然后结果缓存在对象上。为了防止多个任务重复相同的工作,我们通过原子 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 注入断点。有办法做到这一点吗?或者是否有其他一些我忽略的技术来模拟这种竞争条件?
We have an asynchronous task that performs a potentially long-running calculation for an object. The result is then cached on the object. To prevent multiple tasks from repeating the same work, we added locking with an atomic SQL update:
UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0
The locking is only for the asynchronous task. The object itself may still be updated by the user. If that happens, any unfinished task for an old version of the object should discard its results as they're likely out-of-date. This is also pretty easy to do with an atomic SQL update:
UPDATE objects SET results = '...' WHERE id = 1234 AND version = 1
If the object has been updated, its version won't match and so the results will be discarded.
These two atomic updates should handle any possible race conditions. The question is how to verify that in unit tests.
The first semaphore is easy to test, as it is simply a matter of setting up two different tests with the two possible scenarios: (1) where the object is locked and (2) where the object is not locked. (We don't need to test the atomicity of the SQL query as that should be the responsibility of the database vendor.)
How does one test the second semaphore? The object needs to be changed by a third party some time after the first semaphore but before the second. This would require a pause in execution so that the update may be reliably and consistently performed, but I know of no support for injecting breakpoints with RSpec. Is there a way to do this? Or is there some other technique I'm overlooking for simulating such race conditions?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
您可以借鉴电子制造的想法,并将测试挂钩直接放入生产代码中。正如电路板可以在制造时留出特殊位置供测试设备控制和探测电路一样,我们也可以用代码做同样的事情。
假设我们有一些代码将一行插入数据库:
但是该代码在多台计算机上运行。那么,存在竞争条件,因为另一个进程可能会在我们的测试和插入之间插入行,从而导致 DuplicateKey 异常。我们想要测试我们的代码是否处理由该竞争条件导致的异常。为此,我们的测试需要在调用
row_exists?
之后、调用insert_row
之前插入行。因此,让我们在此处添加一个测试钩子:当在野外运行时,该钩子除了消耗一点点 CPU 时间之外什么也不做。但是,当测试代码的竞争条件时,测试猴子补丁 before_insert_row_hook:
这不是很狡猾吗?就像寄生黄蜂幼虫劫持了毫无戒心的毛毛虫的身体一样,测试劫持了被测代码,以便它将创建我们需要测试的确切条件。
这个想法就像XOR游标一样简单,所以我怀疑很多程序员都独立发明了它。我发现它对于测试具有竞争条件的代码通常很有用。我希望它有帮助。
You can borrow an idea from electronics manufacturing and put test hooks directly into the production code. Just as a circuit board can be manufactured with special places for test equipment to control and probe the circuit, we can do the same thing with the code.
SUppose we have some code inserting a row into the database:
But this code is running on multiple computers. There's a race condition, then, since another processes may insert the row between our test and our insert, causing a DuplicateKey exception. We want to test that our code handles the exception that results from that race condition. In order to do that, our test needs to insert the row after the call to
row_exists?
but before the call toinsert_row
. So let's add a test hook right there:When run in the wild, the hook does nothing except eat up a tiny bit of CPU time. But when the code is being tested for the race condition, the test monkey-patches before_insert_row_hook:
Isn't that sly? Like a parasitic wasp larva that has hijacked the body of an unsuspecting caterpillar, the test hijacked the code under test so that it will create the exact condition we need tested.
This idea is as simple as the XOR cursor, so I suspect many programmers have independently invented it. I have found it to be generally useful for testing code with race conditions. I hope it helps.
感谢韦恩的想法。猴子修补的唯一问题是,这将适用于整个套件中对测试方法的所有调用,这可能不是您想要的。
使用代码块代替怎么样?这样,您可以在单独的单元测试中提供一个块,但在其他地方保持相同的实现。
测试中(以RSpec为例)
Thanks to Wayne for the idea. The only trouble with monkey patching is that this will apply to all calls to the method under test throughout your suite, which is probably not what you want.
How about using a code block instead? That way, you can provide a block in an individual unit test but leave the implementation the same everywhere else.
In the test (using RSpec as an example)