【ScalaTest系列3】 使用断言示例使用词典
使用断言
ScalaTest默认提供了三种断言方式,可以在任何样式特性中使用:
- assert用于一般的断言;
- assertResult用于区分期望值和实际值;
- assertThrows用于确保代码抛出了预期的异常。
为了快速开始使用ScalaTest,请学习并使用这三种断言方式。稍后如果您愿意,可以切换到更具表达力的匹配器DSL。
方法总结
ScalaTest的断言定义在Assertions特性中,该特性被Suite特性扩展,是所有样式特性的超特性。Assertions特性还提供了以下方法:
- assume用于有条件地取消测试;
- fail用于无条件地使测试失败;
- cancel用于无条件地取消测试;
- succeed用于无条件地使测试成功;
- intercept用于确保代码抛出了预期的异常,并对异常进行断言;
- assertDoesNotCompile用于确保代码不会编译通过;
- assertCompiles用于确保代码能够编译通过;
- assertTypeError用于确保代码由于类型(而不是解析)错误而无法编译通过;
- withClue用于在失败时添加更多关于错误的信息。
下面详细介绍了这些构造。
示例
assert宏
在任何Scala程序中,您可以通过调用assert并传入一个布尔表达式来编写断言,例如:
val left = 2
val right = 1
assert(left == right)
如果传递的表达式为true,assert将正常返回。如果为false,Scala的assert会抛出AssertionError异常。这个行为是由Predef对象中定义的assert方法提供的,它的成员被隐式地导入到每个Scala源文件中。Assertions特性定义了另一个assert方法,隐藏了Predef中的那个。它的行为相同,只是如果传递false,它会抛出TestFailedException而不是AssertionError。为什么?因为与AssertionError不同,TestFailedException携带了有关哪一行测试代码在堆栈跟踪中表示失败的准确信息,这可以帮助用户更快地找到错误的代码行。此外,ScalaTest的assert比Scala的assert提供更好的错误消息。
如果您在ScalaTest测试中将前面的布尔表达式left == right
传递给assert,将报告一个失败,因为assert被实现为宏,其中包括了报告左值和右值的详细信息。例如,给定上述相同的代码,但使用ScalaTest的断言:
import org.scalatest.Assertions._
val left = 2
val right = 1
assert(left == right)
这个assert抛出的TestFailedException中的详细消息将为:“2 did not equal 1”。
ScalaTest的assert宏通过识别传递给assert的表达式的AST中的模式,并对于有限集的常见表达式,给出了与等效的ScalaTest匹配器表达式相同的错误消息。下面是一些示例,其中a为1,b为2,c为3,d为4,xs为List(a, b, c),num为1.0:
assert(a == b || c >= d)
// 错误消息: 1 did not equal 2, and 3 was not greater than or equal to 4
assert(xs.exists(_ == 4))
// 错误消息: List(1, 2, 3) did not contain 4
assert("hello".startsWith("h") && "goodbye".endsWith("y"))
// 错误消息: "hello" started with "h", but "goodbye" did not end with "y"
assert(num.isInstanceOf[Int])
// 错误消息: 1.0 was not instance of scala.Int
assert(Some(2).isEmpty)
// 错误消息: Some(2) was not empty
对于无法识别的表达式,该宏当前会打印(解糖后的)AST的字符串表示,并添加"was false"。以下是一些未识别表达式的错误消息示例:
assert(None.isDefined)
// 错误消息: scala.None.isDefined was false
assert(xs.exists(i => i > 10))
// 错误消息: xs.exists(((i: Int) => i.>(10))) was false
您可以通过将一个字符串作为assert的第二个参数来增加标准错误消息,例如:
val attempted = 2
assert(attempted == 1, "Execution was attempted " + left + " times instead of 1 time")
使用这种形式的assert,失败报告将更具体地针对您的问题域,从而帮助您调试问题。此Assertions特性还混入了TripleEquals特性,它提供了一个===运算符,可以自定义相等性、使用数字容差进行相等性检查,并在编译时强制执行类型约束。
期望结果
尽管assert宏为Scala的断言机制提供了一种自然、可读性好且提供良好错误消息的扩展,但是当操作数变得冗长时,代码的可读性会降低。此外,对于和=比较生成的错误消息没有区分实际值和期望值。操作数只被称为left和right,因为如果一个被命名为expected而另一个被命名为actual,人们很难记住哪个是哪个。为了解决这些断言的限制,Suite包含了一个名为assertResult的方法,可以用作assert的替代方法。要使用assertResult,您将期望值放在assertResult之后的括号中,然后是包含应产生期望值的代码的大括号。例如:
val a = 5
val b = 2
assertResult(2) {
a - b
}
在这种情况下,期望值为2,被测试的代码是a - b
。这个断言将失败,并且在TestFailedException的详细消息中将读到:“Expected 2, but got 3”。
强制失败
如果您只需要使测试失败,可以写:
fail()
或者,如果您希望测试失败并附带一条消息,请写:
fail("I've got a bad feeling about this.")
取得成功
在异步样式的测试中,您必须用Future[Assertion]或Assertion来结束您的测试主体。ScalaTest的断言(包括匹配器表达式)具有Assertion结果类型,因此以断言结束将满足编译器的要求。然而,如果传递给Future.map的测试主体或函数主体不以Assertion类型结尾,您可以通过在测试主体或函数主体的末尾放置succeed来修复类型错误:
succeed // 结果类型为Assertion
期望异常
有时候,您需要测试一个方法在特定情况下是否抛出了预期的异常,比如当给方法传递无效的参数时。您可以按照JUnit 3风格这样做,例如:
val s = "hi"
try {
s.charAt(-1)
fail()
}
catch {
case _: IndexOutOfBoundsException => // 预期的情况,继续执行
}
如果charAt按预期抛出IndexOutOfBoundsException的实例,控制将转移到catch语句块,该语句块什么也不做。然而,如果charAt没有抛出异常,下一条语句fail()将会执行。fail方法始终以TestFailedException完成,从而表示测试失败。
为了更容易表达和阅读这种常见用例,ScalaTest提供了两个方法:assertThrows和intercept。以下是使用assertThrows的示例:
val s = "hi"
assertThrows[IndexOutOfBoundsException] { // 结果类型为Assertion
s.charAt(-1)
}
这段代码的行为与前面的例子类似。如果charAt抛出了一个IndexOutOfBoundsException实例,assertThrows将返回Succeeded。但是,如果charAt正常完成或抛出了其他异常,assertThrows将以TestFailedException结束。
intercept方法的行为与assertThrows相同,只是intercept不返回Succeeded,而是返回捕获到的异常,以便您可以进一步检查它(如果需要)。例如,您可能需要确保异常中包含的数据具有预期的值。以下是一个示例:
val s = "hi"
val caught =
intercept[IndexOutOfBoundsException] { // 结果类型为IndexOutOfBoundsException
s.charAt(-1)
}
assert(caught.getMessage.indexOf("-1") != -1)
检查代码是否编译通过
在创建库时,通常希望确保某些代码的排列表示潜在的“用户错误”,以便您的库更加抗错。ScalaTest的Assertions特性提供了以下语法来实现此目的:
assertDoesNotCompile("val a: String = 1")
如果要确保由于类型错误(而不是语法错误)而无法编译通过的代码,可以使用:
assertTypeError("val a: String = 1")
请注意,assertTypeError调用仅在给定的代码片段由于类型错误而无法编译通过时才会成功。语法错误仍然会导致抛出TestFailedException。
如果要声明一段代码可以编译通过,您可以使用以下方式更明显地表达:
assertCompiles("val a: Int = 1")
尽管前面三个构造是使用宏实现的,在编译时确定了由字符串表示的代码片段是否能够编译通过,但是错误仍然作为运行时的测试失败报告。
假设
Assertions特性还提供了几种方法,允许您取消测试。如果测试需要一个不可用的资源,您将取消测试。例如,如果测试需要一个外部数据库在线,并且没有在线,可以取消测试以指示由于缺少数据库而无法运行。这样的测试假定数据库可用,您可以在测试开始时使用assume方法来指示这一点,例如:
assume(database.isAvailable)
对于每个重载的assert方法,Assertions特性提供了一个具有相同签名和行为的重载assume方法,除了assume方法会抛出TestCanceledException,而assert方法会抛出TestFailedException。与assert一样,assume隐藏了Predef中的一个Scala方法,该方法执行类似的功能,但会抛出AssertionError。与assert一样,您将从传递给assume的AST中提取的错误消息,可以选择提供一个提示字符串来增加这个错误消息。以下是一些示例:
assume(database.isAvailable, "The database was down again")
assume(database.getAllUsers.count === 9)
强制取消
对于每个重载的fail方法,都有一个相应的cancel方法,具有相同的签名和行为,只是cancel方法抛出TestCanceledException,而fail方法抛出TestFailedException。因此,如果您只需要取消测试,可以写:
cancel()
如果要使用一条消息取消测试,请将消息放在括号中:
cancel("Can't run the test because no internet connection was found")
获取线索
如果您想要比该特性的方法默认提供的信息更多的信息,可以通过多种方式提供一个“clue”字符串。您提供的额外信息(或“线索”)将包含在抛出异常的详细消息中。assert和assertResult还提供了直接包含线索的方法,intercept没有。以下是在assert中直接提供线索的示例:
assert(1 + 1 === 3, "this is a clue")
以及在assertResult中:
assertResult(3, "this is a clue") { 1 + 1 }
前两个语句抛出的异常将在异常的详细消息中包含线索字符串"this is a clue"。要在由失败的assertThrows调用抛出的异常的详细消息中获得相同的线索,需要使用withClue:
withClue("this is a clue") {
assertThrows[IndexOutOfBoundsException] {
"hi".charAt(-1)
}
}
withClue方法只会将线索字符串添加到混入了ModifiableMessage特性的异常类型的详细消息之前。有关更多信息,请参阅ModifiableMessage的文档。如果您希望在代码块之后放置线索字符串,请参阅AppendedClues的文档。
withClue("this is a clue") {
assertThrows[IndexOutOfBoundsException] {
"hi".charAt(-1)
}
}
withClue方法只会将线索字符串添加到混入了ModifiableMessage特性的异常类型的详细消息之前。有关更多信息,请参阅ModifiableMessage的文档。如果您希望在代码块之后放置线索字符串,请参阅AppendedClues的文档。
接下来,了解如何对测试进行标记。
推荐阅读
【ScalaTest系列1】由来场景用法示例详解
【ScalaTest教程2】使用ScalaTest进行测试步骤详解指南
【ScalaTest系列3】 使用断言示例使用词典
【ScalaTest系列4】Sharing fixtures共享测试夹具
【ScalaTest系列5】ScalaTest + JUnit 5+gradle+idea