原标题:关于写异步代码测试用例的一些思考
如果说异步代码不好写是共识的话,那么写异步代码测试用例就更难了。最近我刚刚完成了一个 Flaky 测试,所以想和大家分享一些关于写异步测试用例的想法。
这篇文章里,我们会探索一个关于异步测试用例的常见问题 —— 如何强制规定某些线程的顺序,如何强制某一个线程操作早于另一些执行。通常我们并不想强行规定线程之间的顺序,因为这违背了多线程的原则,所谓多线程就是为了做到并发,从而使得 CPU 可以根据当前照片及应用状态选择最佳的执行顺序。但是在测试中,为了确保测试结果的稳定性,又必须明确线程顺序。
测试节流阀(Throttler)在软件业里节流阀指的是用于限制并发操作个数,预留照片的模式,好比连接池,网络缓存,或者 CPU 密集型操作。和其他同步工具不同的是,节流阀的角色是启动“快速失败”机制,即促使超额请求立即失败,而不是等待。“快速失败”机制之所以重要,是因为切换操作,等待操作会消耗照片 —— 端口,线程,内存等。
以下就是一个节流阀的简单实现(基本上是信号量的包装,实际应用中应该是等待,重试等等)
classThrottledExceptionextendsRuntimeException("Throttled!") classThrottler(count:Int){ privatevalsemaphore=newSemaphore(count) defapply(f:=>Unit):Unit={ if(!semaphore.tryAcquire())thrownewThrottledException try{ f }finally{ semaphore.release() } } }
现在我们开始基本的单元测试:测试单线程的节流阀(我们使用测试框架 specs2)。本例里,我们会验证顺序调用是否会超过节流阀的最大限制(maxCount变量如下所示)。注意,这里我们用的是单线程,所以我们并不验证节流阀的“快速失败”功能,这里的节流阀都处于不饱和状态。事实上,我们只会测试节流阀在不饱和状态下不会终止操作。
classThrottlerTestextendsSpecification{ "Throttler"should{ "executesequential"innewctx{ varinvocationCount=0 for(i<-0tomaxCount){ throttler{ invocationCount+=1 } } invocationCountmustbe_==(maxCount+1) } } traitctx{ valmaxCount=3 valthrottler=newThrottler(maxCount) } }测试并发节流阀
前一个例子里,节流阀处于不饱和状态,因为单线程里节流阀一般都不会饱和。下面我们来测试一下多线程环境下节流阀是否还能工作良好。
设置如下:
vale=Executors.newCachedThreadPool() implicitvalec:ExecutionContext=ExecutionContext.fromExecutor(e) privatevalwaitForeverLatch=newCountDownLatch(1) overridedefafter:Any={ waitForeverLatch.countDown() e.shutdownNow() } defwaitForever():Unit=try{ waitForeverLatch.await() }catch{ case_:InterruptedException=> caseex:Throwable=>throwex }
ExecutionContext 用来构建 Future,waitForever 方法用来持有线程,直到测试结束前的锁释放。接下来的函数里,我们会关闭一个执行服务。
以下就是一个测试节流器多线程行为的例子:
"throwexceptiononcereachedthelimit[naive,flaky]"innewctx{ for(i<-1tomaxCount){ Future{ throttler(waitForever()) } } throttler{}mustthrowA[ThrottledException]
我们创建了 maxCount 个线程(调用 Future{})来调用 waitForever 函数,该函数会一直直到道测试结束。然后我们绕开节流阀执行另一个操作 —— maxCount + 1。预期的行为是,此时应该抛出 ThrottledException 例外。但是,也许预期的例外并不发生,因为接力器的最后的一个调用可能会比 future 里的先执行(future 里会抛出例外,但是这不是预期结果)。
上面这个测试的问题是,在像期望中那样节流阀抛出异常然后导致节流阀被违反之前,我们无法确定所有的线程都已经开始并且在 waitForever 函数中被阻塞。为了修复这个问题,我们需要一些方法去等待所有 future 开始。这有一个我们大多数都很熟悉的一种方法:只要增加一个 sleep 函数等待一些合适的时间。
"throwexceptiononcereachedthelimit[naive,bad]"innewctx{ for(i<-1tomaxCount){ Future{ throttler(waitForever()) } } Thread.sleep(1000) throttler{}mustthrowA[ThrottledException] }
好了,现在这个测试几乎都能通过了,但是这个方法还是错的因为下面这两个原因:
测试持续的时间至少会和我们设置好的”合适的时间”差不多久。
在非常罕见的情况下,比如机器处于高负载的时候,这个合适的时间不一定足够。
如果你仍然感到疑惑,可以搜索一下Google更多的原因。
一个更好的方式是将我们的线程(future)的开始和我们期望的东西同步起来。我们来使用 java.util.concurrent 里面的 CountDownLatch 类:
"throwexceptiononcereachedthelimit[working]"innewctx{ valbarrier=newCountDownLatch(maxCount) for(i<-1tomaxCount){ Future{ throttler{ barrier.countDown() waitForever() } } } barrier.await(5,TimeUnit.SECONDS)mustbeTrue throttler{}mustthrowA[ThrottledException] }
我们使用 CountDownLatch 处理障碍同步。这个等待的方法会阻塞主线程直到锁存计数变为 0。随着其它线程的运行(我们把这些其它线程表示为 future),每一个 future 都会调用 countDown 方法使锁存计数减 1。一但计数变为 0,所有的 future 就都已经运行到 waitForever 方法中了。
通过那一点,我们可以确保 throttler 是饱和的,内部有最大数量(maxCount)的线程。另一个线程试图进入 throttler 将导致异常。我们有一个确定的方式建立我们的测试,测试会有一个主线程进入 throttler。主线程可以恢复到这个点(门闩计数为 0 并等 CountDownLatch 释放等待线程)。
如果一些意想不到的事情发生,我们使用超时略高保障避免无限阻塞发生。如果这样的事情发生,我们的测试就失败了。这个超时不会影响到测试时间,除非发生意外情况,否则,我们都不应该等待。
结论测试异步程序时,通常需要在具体的测试用例中指定多个线程之间的执行顺序。不使用任何同步策略的测试是不可靠的,测试结果有时成功有时失败。使用 Thread.sleep 降低了测试出错的概率,但没有完全解决这个问题。
在大多数情况下,当需要在测试中保证多个线程的执行顺序时,可以使用 CountDownLatch 代替 Thead.sleep。使用 CountDownlatch 的好处是通过它可以指定释放(保持)线程的时机,有两个优点:确保按顺序执行使测试结果更可靠;加快了测试程序的执行速度。即使对于普通的 waiting 操作,比如 waitForever 函数,尽管也可以使用 Thread.sleep(Long.MAX_VALUE) 这样的函数实现,但为了保证程序的健壮性最好不要这样做。
完整的代码可以在GitHub中找到。
相关:
你还记得航天员王亚平、聂海胜、张晓光在“天宫一号”上过的一堂奇妙生动的太空课吗?航天员们轻松自如,面
不少朋友在吃土豆时,会纠结要不要吃土豆皮的问题。不说口感,主要是担心中毒的问题。这主要涉及土豆的一种
本文由玩赚乐(www.banghui.org)– 小峰原创翻译,转载请看清文末的转载要求,欢迎加入技术翻译小组!经过多
在过去的几天里,我有了开发生涯中最有意义的经历之一, 想在这里跟大家分享。现在我们已经让 ClojureScript
理解SpringMVCModelAttribute和SessionAttribute
作为一名 Java Web 应用开发者,你已经快速学习了 request(HttpServletRequest)和 session(HttpSession)
有的,比如,额前叶切除术。葡萄牙神经科医生、科学家安东尼奥·莫尼斯 ,因“发现了脑白质切断术对某
最近,星巴克又出现在科技新闻版面。这背后的缘起,是苹果 Apple Pay 的负责人 Jennifer Bailey 在科技网站
前段时间,和我在同一家餐厅工作的洗碗小哥 Tristan 问我,能不能采访我,人类学专业的他想要了解中国食物,
最后一段或许能够回答你的问题。Test Site Vol.2Superman Earth One超人的故事,最难讲。因为一不小心,你就
作为一个狂热的碳水化合物爱好者,我隆重地向你们推荐一道可以下饭的菜——煎饺。没错,煎饺。而且是与国内