如果说异步代码不好写是共识的话,那么写异步代码测试用例更难了。近我刚刚完成了一个 Flaky 测试,所以想和大家分享一些关于写异步测试用例的想法。
  这篇文章里,我们会探索一个关于异步测试用例的常见问题 —— 如何强制规定某些线程的顺序,如何强制某一个线程操作早于另一些执行。通常我们并不想强行规定线程之间的顺序,因为这违背了多线程的原则,所谓多线程是为了做到并发,从而使得 CPU 可以根据当前资源及应用状态选择佳的执行顺序。但是在测试中,为了确保测试结果的稳定性,又必须明确线程顺序。
  测试节流阀(Throttler)
  在软件业里节流阀指的是用于限制并发操作个数,预留资源的模式,好比连接池,网络缓存,或者 CPU 密集型操作。和其他同步工具不同的是,节流阀的角色是启动“快速失败”机制,即促使超额请求立即失败,而不是等待。“快速失败”机制之所以重要,是因为切换操作,等待操作会消耗资源 —— 端口,线程,内存等。
  以下是一个节流阀的简单实现(基本上是信号量的包装,实际应用中应该是等待,重试等等)
  class ThrottledException extends RuntimeException("Throttled!")
  class Throttler(count: Int) {
  private val semaphore = new Semaphore(count)
  def apply(f: => Unit): Unit = {
  if (!semaphore.tryAcquire()) throw new ThrottledException
  try {
  f
  } finally {
  semaphore.release()
  }
  }
  }
  现在我们开始基本的单元测试:测试单线程的节流阀(我们使用测试框架 specs2)。本例里,我们会验证顺序调用是否会超过节流阀的大限制(maxCount变量如下所示)。注意,这里我们用的是单线程,所以我们并不验证节流阀的“快速失败”功能,这里的节流阀都处于不饱和状态。事实上,我们只会测试节流阀在不饱和状态下不会终止操作。
  class ThrottlerTest extends Specification {
  "Throttler" should {
  "execute sequential" in new ctx {
  var invocationCount = 0
  for (i <- 0 to maxCount) {
  throttler {
  invocationCount += 1
  }
  }
  invocationCount must be_==(maxCount + 1)
  }
  }
  trait ctx {
  val maxCount = 3
  val throttler = new Throttler(maxCount)
  }
  }
  测试并发节流阀
  前一个例子里,节流阀处于不饱和状态,因为单线程里节流阀一般都不会饱和。下面我们来测试一下多线程环境下节流阀是否还能工作良好。
  设置如下:
  val e = Executors.newCachedThreadPool()
  implicit val ec: ExecutionContext=ExecutionContext.fromExecutor(e)
  private val waitForeverLatch = new CountDownLatch(1)
  override def after: Any = {
  waitForeverLatch.countDown()
  e.shutdownNow()
  }
  def waitForever(): Unit = try {
  waitForeverLatch.await()
  } catch {
  case _: InterruptedException =>
  case ex: Throwable => throw ex
  }
  ExecutionContext 用来构建 Future,waitForever 方法用来持有线程,直到测试结束前的锁释放。接下来的函数里,我们会关闭一个执行服务。