2KB项目,专业的源码交易网站 帮助 收藏 每日签到

测试异步代码

  • 时间:2019-01-23 18:30 编辑:2KB 来源:2KB.COM 阅读:329
  • 扫一扫,手机访问
  • 分享
摘要: 英文原文:Tes
英文原文:Testing Asynchronous Code

如果说异步代码不好写是共识的话,那么写异步代码测试用例就更难了。最近我刚刚完成了一个 Flaky 测试,所以想和大家分享一些关于写异步测试用例的想法。

这篇文章里,我们会探索一个关于异步测试用例的常见问题 —— 如何强制规定某些线程的顺序,如何强制某一个线程操作早于另一些执行。通常我们并不想强行规定线程之间的顺序,因为这违背了多线程的原则,所谓多线程就是为了做到并发,从而使得 CPU 可以根据当前资源及应用状态选择最佳的执行顺序。但是在测试中,为了确保测试结果的稳定性,又必须明确线程顺序。

其它翻译版本 (2) 加载中

测试节流阀(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 方法用来持有线程,直到测试结束前的锁释放。接下来的函数里,我们会关闭一个执行服务。

以下就是一个测试节流器多线程行为的例子:

"throw exception once reached the limit [naive,flaky]" in new ctx {
  for (i <- 1 to maxCount) {
    Future {
      throttler(waitForever())
    }
  }
  throttler {} must throwA[ThrottledException]

我们创建了 maxCount 个线程(调用 Future{})来调用 waitForever 函数,该函数会一直直到道测试结束。然后我们绕开节流阀执行另一个操作 —— maxCount + 1。预期的行为是,此时应该抛出 ThrottledException 例外。但是,也许预期的例外并不发生,因为接力器的最后的一个调用可能会比 future 里的先执行(future 里会抛出例外,但是这不是预期结果)。

上面这个测试的问题是,在像期望中那样节流阀抛出异常然后导致节流阀被违反之前,我们无法确定所有的线程都已经开始并且在 waitForever 函数中被阻塞。为了修复这个问题,我们需要一些方法去等待所有 future 开始。这有一个我们大多数都很熟悉的一种方法:只要增加一个 sleep 函数等待一些合适的时间。

"throw exception once reached the limit [naive, bad]" in new ctx {
  for (i <- 1 to maxCount) {
    Future {
      throttler(waitForever())
    }
  }
  Thread.sleep(1000)
  throttler {} must throwA[ThrottledException]
}

好了,现在这个测试几乎都能通过了,但是这个方法还是错的因为下面这两个原因:

测试持续的时间至少会和我们设置好的"合适的时间"差不多久。

在非常罕见的情况下,比如机器处于高负载的时候,这个合适的时间不一定足够。

如果你仍然感到疑惑,可以搜索一下 Google 更多的原因。

一个更好的方式是将我们的线程(future)的开始和我们期望的东西同步起来。我们来使用 java.util.concurrent 里面的 CountDownLatch 类:

"throw exception once reached the limit [working]" in new ctx {
  val barrier = new CountDownLatch(maxCount)
 
  for (i <- 1 to maxCount) {
    Future {
      throttler {
        barrier.countDown()
        waitForever()
      }
    }
  }
 
  barrier.await(5, TimeUnit.SECONDS) must beTrue
 
  throttler {} must throwA[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 中找到。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。


2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务

  • 全部评论(0)
资讯详情页最新发布上方横幅
最新发布的资讯信息
【计算机/互联网|】Nginx出现502错误(2020-01-20 21:02)
【计算机/互联网|】网站运营全智能软手V0.1版发布(2020-01-20 12:16)
【计算机/互联网|】淘宝这是怎么了?(2020-01-19 19:15)
【行业动态|】谷歌关闭小米智能摄像头,因为窃听器显示了陌生人家中的照片(2020-01-15 09:42)
【行业动态|】据报道谷歌新闻终止了数字杂志,退还主动订阅(2020-01-15 09:39)
【行业动态|】康佳将OLED电视带到美国与LG和索尼竞争(2020-01-15 09:38)
【行业动态|】2020年最佳AV接收机(2020-01-15 09:35)
【行业动态|】2020年最佳流媒体设备:Roku,Apple TV,Firebar,Chromecast等(2020-01-15 09:31)
【行业动态|】CES 2020预览:更多的流媒体服务和订阅即将到来(2020-01-08 21:41)
【行业动态|】从埃隆·马斯克到杰夫·贝佐斯,这30位人物定义了2010年代(2020-01-01 15:14)
联系我们

Q Q: 7090832

电话:400-0011-990

邮箱:7090832@qq.com

时间:9:00-23:00

联系客服
商家入住 服务咨询 投拆建议 联系客服
0577-67068160
手机版

扫一扫进手机版
返回顶部