Scalatest异步测试中如何实现多条件断言并快速失败?
解决Scalatest异步测试的编译问题与需求实现
问题原因
原代码中map块的最后一个表达式是SomeGlobalContextHolder.ctx.foreach(...),而foreach返回Unit,导致整个map的结果类型是Unit,最终返回Future[Unit],但Scalatest的AsyncFlatSpec要求测试用例必须返回Future[Assertion],因此出现编译错误。
解决方案
以下两种方案均能满足你的三个需求:编译通过、条件执行断言、快速失败。
方案1:直接执行断言并返回succeed
这种方案简洁直观,利用Scalatest断言失败即抛出异常的特性实现快速失败:
import org.scalatest.flatspec.AsyncFlatSpec import org.scalatest.matchers.should.Matchers._ import scala.concurrent.Future class MultipleOptionAsyncTests extends AsyncFlatSpec { object SomeGlobalContextHolder { val ctx: Option[SomeGlobalContext] = Some(new SomeGlobalContext) } class SomeGlobalContext { def apply(key: String): Set[String] = Set("values that have been set for the key") } case class User(id: String, ssoId: Option[String]) type TypeOfResult = String def methodBeingTested(user: User): Future[TypeOfResult] = Future.successful("some result") it should "get compiled and test ALL assertions" in { val user = User("id", Some("ssoId")) methodBeingTested(user).map { result => // 基础断言:失败则立即终止测试 result shouldBe "some result" // 条件断言:仅当ctx存在时执行 SomeGlobalContextHolder.ctx.foreach { ctx => // 仅当ssoId存在时执行该断言 user.ssoId.foreach { ssoId => ctx("ssoId") should contain(ssoId) } // 无论ssoId是否存在,都执行该断言 ctx("user") should contain(user.id) } // 返回Assertion,满足编译要求 succeed } } }
方案2:组合所有断言为一个Assertion
这种方案将所有断言(包括条件断言)组合成一个Assertion,更符合Scalatest的断言组合风格:
import org.scalatest.flatspec.AsyncFlatSpec import org.scalatest.matchers.should.Matchers._ import scala.concurrent.Future class MultipleOptionAsyncTests extends AsyncFlatSpec { object SomeGlobalContextHolder { val ctx: Option[SomeGlobalContext] = Some(new SomeGlobalContext) } class SomeGlobalContext { def apply(key: String): Set[String] = Set("values that have been set for the key") } case class User(id: String, ssoId: Option[String]) type TypeOfResult = String def methodBeingTested(user: User): Future[TypeOfResult] = Future.successful("some result") it should "get compiled and test ALL assertions" in { val user = User("id", Some("ssoId")) methodBeingTested(user).map { result => // 基础断言 val baseAssertion = result shouldBe "some result" // 组合条件断言 val conditionalAssertions = SomeGlobalContextHolder.ctx.fold(succeed) { ctx => // ssoId存在时执行断言,否则返回succeed(无断言) val ssoAssertion = user.ssoId.fold(succeed)(ssoId => ctx("ssoId") should contain(ssoId)) // 必执行的断言 val userAssertion = ctx("user") should contain(user.id) // 组合两个断言 ssoAssertion and userAssertion } // 组合所有断言,返回最终的Assertion baseAssertion and conditionalAssertions } } }
需求满足说明
- 编译通过:两种方案的
map块最终都返回Assertion类型,因此Future的类型为Future[Assertion],符合Scalatest的要求。 - 条件执行断言:通过
Option.foreach(方案1)或Option.fold(方案2)实现仅当Option有值时执行对应断言。 - 快速失败:Scalatest的断言失败会立即抛出
TestFailedException,导致Future进入失败状态,测试会立即终止,不会执行后续断言。
内容的提问来源于stack exchange,提问作者danissimo




