当您必须测试多线程程序时,总是需要等到系统达到特定状态后,测试才能验证是否达到了正确的状态。
这样做的通常方法是在系统中插入一个“探针”,该探针将向同步原语发出信号 (例如Semaphore ),并且测试将一直等到信号量得到信号或超时通过。 (您永远不应该做的两件事,但是经常犯错误,就是将睡眠插入代码中(因为它们会使您变慢并且变得脆弱),或者使用Object.wait方法而不进行循环处理(因为您可能会感到虚假)唤醒会导致虚假,难以诊断和非常令人沮丧的测试失败。
这一切都很好,也很不错(尽管有些冗长-至少要等到Java 8 lambda到达为止),但是如果第二个线程调用了第三个线程并且不等待它完成,该怎么办,但是在测试中我们要等待为了它? 一个具体的例子是:集成测试,它验证由客户端组成的,通过消息中间件与数据网格进行通信的系统是否将数据正确写入数据网格? 当然,我们将使用模拟中间件和模拟数据网格,因此启动/关闭和处理将非常快,但它们仍将是异步的(假设由于生产环境不是同步的而我们无法使其同步)编写代码以使其依赖于此事实)。
在下面的序列图中直观地描述了这种情况:我们在T0上运行了测试,我们希望它等待T3上的任务完成后再检查系统到达的状态。
我们可以通过对执行框架(可能是某种执行器)进行少量修改来实现此目的。 给定以下界面:
public interface ActivityCollector {void before(); void after();
}
我们将在任务排队等待执行时调用before()
,在执行任务after()
调用after()
(这些通常会在不同的线程上发生)。 如果现在我们考虑在增加计数器之前和之后减少计数器,我们可以等待计数器变为零(具有适当的同步),此时我们知道所有任务都已由系统处理。 您可以在此处找到执行此功能的执行程序。 在生产中,您当然可以使用不执行任何操作的接口实现,从而消除了任何性能开销。
现在让我们看一下定义我们如何等待“已处理”条件的接口:
interface ActivityWatcher {void await(long time, TimeUnit timeUnit);
}
这里使用的两个个人设计选择是:仅提供一种等待特定时间且不再等待的方法(如果测试花费的时间太长,则可能需要考虑性能下降),并使用未经检查的异常来编写测试代码较短。
最后一个功能是在任务执行期间收集异常,如果某处存在异常,则立即中止而不是超时。 这意味着我们将界面修改如下:
public interface ActivityCollector {void before(); void after();void collectException(Throwable t);
}
包装执行的代码如下所示:
try {command.run();
} catch (Throwable t) {activityCollector.collectException(t);throw t;
} finally {activityCollector.after();
}
您可以在此处找到ActivityWatcher / ActivityCollector的实现(它们之间是非常紧密的联系,因此一个类同时实现了两者)。 测试愉快!
注意事项:
- 这需要对生产代码进行一些修改,因此它可能不是最佳的解决方案(例如,您可以尝试创建子系统的同步模拟并以这种方式进行测试)。
- 该解决方案不适用于涉及计时器的情况,因为有时“没有任务在等待”,但实际上某个任务正在计时器中等待。 您可以通过使用自定义计时器来解决此问题,该计时器在计划任务时调用“ before”,在任务完成时调用“ after”。
- 如果您使用网络通信以提高可靠性(即使它位于同一进程中),可能会出现相同的问题:有时会因为未在系统网络中序列化任务而没有安排任何任务。
- ActivityCollector是单个同步点。 因此,这可能会降低性能,并可能隐藏并发错误。 有很多更复杂的方法可以实现它,从而避免了一些同步开销(例如使用ConcurrentLinkedQueue),但是您不能完全消除它。
PS。 该示例基于我似乎找不到的IBM文章(亲爱的lazyweb:如果有人找到它,请在其中打上“ tick / tock”之前/之后发表评论)以及我的同事的工作。 我唯一的角色是编写并合成它。
参考: 等待正确的时机–在 Java Advent Calendar博客上,我们的JCG合作伙伴 Attila-Mihaly Balazs进行了集成测试 。
翻译自: https://www.javacodegeeks.com/2012/12/waiting-for-the-right-moment-in-integration-testing.html