1. 概述
ScalaTest是scala生态系统中最流行和灵活的测试工具,可以测试scala、js、java代码。
2. ScalaTest的特性
a. ScalaTest的核心是套件(suite),即0到多个测试的集合
b. 测试可以是含有一个名称的任意内容,该名称可以用来启动、待处理或取消,也可表示成功或失败等
c. trait Suite声明run和其他"生命周期"的方法,这些方法定义编写和执行测试的默认方式;"生命周期"方法可被重写,以定制测试如何编写和运行
d. ScalaTest提供继承Suite的样式traits,并且重写生命周期方法来支持不同的测试类型。它提供了混合(mixin)特性,重写生命周期方法,以满足特定测试需求
e. 你可以通过组合Suite样式和混合traits来定义测试类;可以通过组合Suite实例来定义测试套件。
3. Maven依赖
Maven项目中增加ScalaTest,只需引入如下依赖即可
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<version>3.0.8</version>
<scope>test</scope>
</dependency>
4. 选取测试样式的不成文约定
a. 推荐为每个项目选择一组满足团队的测试样式,同时保持项目代码的一致性
b. 推荐为单元测试选择一个主样式,而验收测试选择另一样式
c. 一般情况下,推荐使用FlatSpec样式用于单元测试和集成测试,FeatureSpec用于验收测试。
注意:选择的样式只是表明测试声明的外观,无论选择哪种样式,ScalaTest中的其他内容,均以相同的方式工作
5. 样式Trait
(1) FunSuite和XUnit类似,可以轻松编写描述性测试名称,自然地编写集中测试,并生成类似规范输出,促进相关利益者沟通。
(2) FlatSpec的结构类似于XUnit,但是测试名称必须写成规定样式,如X should Y, A must be等
(3) FunSpec类似于Ruby的RSpec工具,对于偏好BDD的团队来说,FunSpec的嵌套和温和的结构化文本指南(使用describe和it)为编写规范式测试提供了极好的通用选择。
(4) 对于来资源specs或spec2的团队,WordSpec将会感觉很熟悉。WordSpec在如何编写文本方面非常规范,因此非常适合于希望在规范文本上强制执行高度管理的团队。
(5) FreeSpec在如何编写规范文档中方面相对自由
(6) PropSpec适合想要在属性检查方面专门编写测试的团队,当选择不同的样式特征作为主要单元测试样式时,也是编写偶尔测试矩阵的好选择
(7) FeatureSpec主要用于验收测试,包括促进程序员与非程序员一起工作以确定验收要求的过程
(8) RefSpec允许将测试类定义为方法,与将测试表示为函数的样式类相比,每个测试保存一个函数文字。更少的函数文字转换为更快的编译时间和更少的生成的类文件,这可以帮助最小化构建时间。
因此,在构建时间受到关注的大型项目中以及通过静态代码生成器以编程方式生成大量测试时,使用Spec可能是一个不错的选择。
6. 定义基类
(1) 为工程创建你最常使用混合特性的抽象基类,而非重复的复制代码来混合相同的trait,如:
a. 创建抽象基类
import org.scalatest._
abstract class UnitSpec extends FlatSpec with Matchers with OptionValues with Inside with Inspectors
b. 继承抽象类
import org.scalatest._
class MySpec extends UnitSpec{
// 测试类
}
7. 编写第一个测试
(1) 使用ScalaTest时,定义一个类,且继承一个样式类如FlatSpec
(2) 在FlatSpec中的每个测试由句子构成,该句子指定了一些所需行为和一个测试它的代码块。
a. 该句子需要一个主题,如" A Stack"
b. 一个动词,如should, must, can
示例:"A Stack" should "pop values in last-in-first-out order"
c. 如果多个测试的主题相同,可以使用it来指代之前的主题,如:
it should "throw NoSuchElementException if an empty stack is popped"
d. 句子后面需要增加"in",其后紧跟着以{}括起来的测试代码示例:
package com.ws.spark.study.scalatest import org.scalatest.FlatSpec import scala.collection.mutable class StackSpec extends FlatSpec{ "A Stack" should "pop values in last-in-first-out order" in { val stack = new mutable.Stack[Int] stack.push(1) stack.push(2) assert(stack.pop() == 2) assert(stack.pop() == 1) } // it 表示之前的主题 "A Stack", 适用于主题相同的情况 it should "throw NoSuchElementException if an empty stack is popped" in { val emptyStack = new mutable.Stack[Int] assertThrows[NoSuchElementException]{ emptyStack.pop() } } }
8. 使用断言
(0) 参考:
package com.ws.spark.study.scalatest import org.scalatest.FlatSpec import scala.collection.mutable class AssertionTest extends FlatSpec{ /********************************* assert macro *************************************/ /**************************************************/ val left = 2 val right = 1 // 将会打印" 2 did not equal 1 "错误 // assert(left == right) /**************************************************/ val a = 1 val b = 2 val c = 3 val d = 4 val xs = List(a, b, c) val num = 1.0 // 打印"1 did not equal 2, and 3 was not greater than or equal to 4" // assert(a == b || c >=d ) // List(1, 2, 3) did not contain 4 // assert(xs.contains(4)) // "help" started with "h", but "goodbye" did not end with "y" // assert("help".startsWith("h") && "goodbye".endsWith("y")) // 1.0 was not instance of scala.Int // assert(num.isInstanceOf[Int]) // Some(2) was not empty // assert(Some(2).isEmpty) /**************************************************/ // 对于不认识的表达式,assert打印string类型信息,并且增加"was false" // scala.None.isDefined was false // assert(None.isDefined) // AssertsionTest.this.xs.exists(((i: Int) => i.>(10))) was false // assert(xs.exists(i => i > 10)) /**************************************************/ val attempted = 2 // assert(attempted == 1, "Execution was attempted " + left + " times instead of 1 time") /********************************* Expected results *************************************/ val x = 5 val y = 2 // Expected 2, but got 3 // assertResult(2) { // x - y // } /********************************* Forcing failures *************************************/ // fail() // fail("I've got a bad feeling about this") /********************************* Achieving success *************************************/ succeed /********************************* Expected exceptions *************************************/ val s = "hi" // 当charAt抛出索引越界异常,assertThrows将返回Succeed // 当charAt正常结束或返回另一个异常时,assertThrows将会立即结束,且抛出TestFailedException assertThrows[IndexOutOfBoundsException] { s.charAt(-1) // Result type: Assertion } val caught = intercept[IndexOutOfBoundsException] { s.charAt(-1) // Result Type: IndexOutOfBoundsException } // caught.getMessage: String index out of range: -1 assert(caught.getMessage.indexOf("-1") != -1) /********************************* Checking that a snippet of code does or does not compile *************************************/ assertDoesNotCompile("val a: String = 1") assertTypeError("val s: String = 1") assertCompiles("val a: Int = 1") /********************************* Assumptions *************************************/ val m = 10 val n = 10 assume(m === n, ", m must equal n") /********************************* Forcing cancelations *************************************/ // cancel() // cancel("Can't run the test because no internet connection was found") /********************************* Forcing cancelations *************************************/ withClue("this is a clue"){ assertThrows[IndexOutOfBoundsException] { "hi".charAt(-1) } assertThrows[NoSuchElementException]{ val stack = new mutable.Stack[Int] stack.pop() } } }
(1) ScalaTest在任意类型的trait中默认均有三种断言:
a. assert: 通用断言
b. assertResult: 区分预期值与实际值
c. assertThrows: 确保代码块抛出期望异常
(2) ScalaTest中的断言在trait Assertions中定义,也提供了如下方法:
a. assume: 有条件的取消测试
b. fail: 无条件测试失败
c. cancel: 无条件取消测试
d. succeed: 无条件测试成功
e. intercept: 确保代码块抛出期望异常,然后对异常进行断言
f. assertDoesNotCompile: 确保代码块不编译
g. assertCompiles: 确保代码编译
h. assertTypeError: 确保代码由于类型错误而不被编译
i. withClue: 当失败时增加更多信息
(3) assert宏(macro)
a. assert宏的工作原理是识别传递给assert的表达式AST中的模式。对于有限的公共表达式集,给出一条错误信息,等效于ScalaTest的matcher表达式提供的信息。
b. 对于无法识别的表达式,则会打印string信息,并且在其后增加"was false"
c. 也可以增加错误额外信息,通过在assert方法的第二个参数增加
(4) 期望值
a. 通过assertResult实现期望值的断言,使用方法为assertResult的参数位置为期望值,其后跟着花括号包裹的代码,代码执行结果为指定的期望值
(5) 强制失败
a. 如果想测试失败,可以直接写fail(),也可以将错误信息增加到fail参数中
(6) 成功实现
a. succeed可以用于异步测试中没有以Future[Assertion]或Assertion结尾的类型错误
(7) 期望异常
a. ScalaTest提供了两个方法来检测方法抛出的指定异常:assertThrows和intercept。intercept与assertThrows类似,不同之处在于intercept不返回Succeed类型,而是返回捕获的异常
(8) 检测代码块是否编译
a. 当创建库时期望某些潜在错误不被编译,降低库的错误性,可以使用assertDoesNotCompile
b. 当且仅当类型错误时,为了确保代码块不被编译,可以使用assertTypeError。而语法错误时,仍将抛出TestFailedException
c. 为了确保代码块必须编译,可以使用assertCompiles
(9) 假设
a. 在测试前使用assume方法,当不满足条件时,测试将会退出
b. 当不满足条件时,assume抛出TestCanceledException, 而assert抛出
c. assume方法也自带参数,可以在异常时增加额外信息
(10) 强制取消
a. cancel方法和fail方法类似,不同在于cancel抛出TestCanceledException,而fail抛出TestFailedException
(11) 获取线索
a. assert和assertResult自带线索参数, 而intercept没有
b. 如若在调用assertThrows抛出的失败异常的详细信息中,获取相同的线索,需要使用withClue
c. withClue方法仅在混合ModifiableMessages trait的异常类型详情中附加线索信息。
9. "标记"测试
(1) ScalaTest允许定义任意测试类别,将测试"标记"为属于这些类别,并基于该标记过滤要运行的测试(如有的测试执行时间较长)
(2) ScalaTest默认支持ignore标记,可以"短暂"使一个测试不执行。在FlatSpec样式中,可以将it或in替代为ignore
(3) ScalaTest支持自定义"标记"测试,在FlatSpec中,可以在in之前将抽象类org.scalatest.tag扩展到taggedas的对象传递给它。类tag接收一个string参数,作为名称
10. 测试装置
(1) 当多个测试共用相同的装置(如文件, 套接字, 数据库连接等),需要避免测试中的重复装置代码。
(2) ScalaTest推荐了3种减少重复代码的方法:
1) 当不同的测试需要不同的装置时,使用Scala重构
a. get-fixture-methods: 重构extract方法可以为你在每个所需测试中提供可变装置对象的新实例,但结束时不会清理
参考:com.ws.spark.study.scalatest.fixtures.GetFixtureTest
b. fixture-context-objects: 将装置的方法和属性放置到trait中,可以将trait混合,进而为每个测试提供所需的新创建的trait。适用于不同测试中需要可变装置对象的不同组合,且结束时不需要清理
参考:com.ws.spark.study.scalatest.fixtures.FxiturContextTest
c. loan-fixture-methods: 当不同测试需要不同装置,且必须结束时进行清理,可通过loan模式重构重复代码
2) 当大部分测试需要相同装置时,重载withFixture【推荐使用】
a. withFixture(NoArgTest):
该方法允许在大部分测试的开头与结尾执行边缘检测、转换测试结果、重试测试、基于测试名称,标记,或测试数据来做决策。
该方法不适于:
* 不同测试需要不同的装置 => 可使用scala重构
* 装置代码中的异常应终止suite,而非测试失败 => 可使用before-and-after
* 有需要传递到测试中的对象 => 可重载withFixture(OneArgTest)
b. withFixture(OneArgTest): 使用于当需要将相同装置对象作为参数传入大部分测试的场景
3). 当代码失败时,想要中止suite,而非测试失败,可以混合before-and-after trait
a. BeforeAndAfter: 当想要在测试之前和之后执行相同边缘检测,而非在测试开始和结束时,使用此模板
b. BeforeAndAfterEach:在测试之前和之后堆叠trait
注意:
继承BeforeAndAfterEach的stacking traits与实现withFixture的traits的不同之处在于:
a. BeforeAndAfterEach中,初始化与清理代码发生在测试的前后。beforeEach或afterEach异常终止时,将被当做一个SuiteAborted事件
b. withFixture中,初始化和清理代码发生在测试的开始与结束。withFixture异常终止时,将被当做测试失败。
11. 共享测试
(1) 有时需要在不同的fixture对象执行相同的测试,在FlatSpec中,首先将需共享的测试放在行为函数中,这些行为函数将在任何FlatSpec
的构建阶段调用,因此它们所包含的测试将被注册为FlatSpec中的测试。
(2) 注意:当使用共享测试时,suite中的每个测试必须有不同的名字。如果在相同的suite中注册了同名的测试,运行时将会报多个测试注册到同名的测试异常。
在FlatSpec中,较好的解决方法是确保行为函数的每个调用都有一个不同的主题。
例如: "A Stack (when empty)" should "be empty" in { assert(emptyStack.empty) }
如果“should be empty”测试被分解为行为函数,那么只要行为函数的每次调用都在不同主题的上下文中,就可以重复调用它。
参考:
package com.ws.spark.study.scalatest.sharetests import org.scalatest.FlatSpec import scala.collection.mutable.ListBuffer class Stack[T] { val MAX = 10 private val buf = new ListBuffer[T] def full: Boolean = buf.size == MAX def empty: Boolean = buf.isEmpty def size: Int = buf.size def push(o: T): Unit ={ if(!full) buf.prepend(o) else throw new IllegalStateException("can't push onto a full stack") } def pop(): T ={ if(!empty) buf.remove(0) else throw new IllegalStateException("can't pop an empty stack") } def peek: T = { if(!empty) buf.head else throw new IllegalStateException("can't pop an empty stack") } override def toString: String = buf.mkString("Stack(", ", ", ")") } /** * 1. 有了共享变量,你可以将不同的测试分解为一个行为函数,行为函数中传入stack fixture,进而可以在测试运行中使用。因此在 * 如下有关Stack的FlatSpec中,将调用行为函数多次 * * 2. 你可以定义一个行为函数,将这些共享测试封装在使用它们的FlatSpec中。但如果想要在不同的FlatSpec之间共享,也可以在混合 * 到使用它们的每个FlatSpec中的单独trait定义它们 */ trait StackBehaviors{ this: FlatSpec => def nonEmptyStack(newStack: => Stack[Int], lastItemAdded: Int): Unit ={ it should "be non-empty" in { assert(!newStack.empty) } it should "return the top item on peek" in { assert(newStack.peek === lastItemAdded) } it should "not remove the top item on peek" in { val stack = newStack val size = stack.size assert(stack.peek === lastItemAdded) assert(stack.size === size) } it should "remove the top item on pop" in { val stack = newStack val size = stack.size assert(stack.pop === lastItemAdded) assert(size === stack.size + 1) } } def nonFullStack(newStack: => Stack[Int]): Unit ={ it should "not be full" in { assert(!newStack.full) } it should "add to the top on push" in { val stack = newStack val size = stack.size stack.push(7) assert(stack.size === size + 1) assert(stack.peek === 7) } } } /** * 1. 给定了如上行为函数,你可以直接调用它们,FlatSpec提供一个DSL,类似于: * it should behave like nonEmptyStack(stackWithOneItem, lastValuePushed) * it should behave like nonFullStack(stackWithOneItem) * * 2. 【不推荐】 * 如果倾向于使用命令行样式更改fixtures,例如混合BeforeAndAfterEach,并在beforeEach重新设置stack变量,则你可以在变量var * 的上下文中编写行为函数。此时,不需要传入stack fixture,因为stack fixture已经在行为函数中的范围。代码如下: * it should behave like nonEmptyStack // assuming lastValuePushed is also in scope inside nonEmptyStack * it should behave like nonFullStack */ class SharedTestSpec extends FlatSpec with StackBehaviors{ // stack fixture创建方法 def emptyStack = new Stack[Int] def fullStack: Stack[Int] = { val stack = new Stack[Int] for(i <- 0 until stack.MAX){ stack.push(i) } stack } def stackWithOneItem: Stack[Int] = { val stack = new Stack[Int] stack.push(9) stack } def stackWithOneItemLessThanCapacity: Stack[Int] = { val stack = new Stack[Int] for(i <- 1 to 9){ stack.push(i) } stack } val lastValuePushed = 9 "A Stack (when empty)" should "be empty" in { assert(emptyStack.empty) } it should "complain on peek" in { intercept[IllegalStateException] { emptyStack.peek } } it should "complain on pop" in { intercept[IllegalStateException]{ emptyStack.pop() } } "A Stack (with one item)" should behave like nonEmptyStack(stackWithOneItem, lastValuePushed) it should behave like nonFullStack(stackWithOneItem) "A Stack (with one item less than capacity)" should behave like nonEmptyStack(stackWithOneItemLessThanCapacity, lastValuePushed) it should behave like nonFullStack(stackWithOneItemLessThanCapacity) "A Stack (full)" should "be full" in { assert(fullStack.full) } it should behave like nonEmptyStack(fullStack, lastValuePushed) it should "complain on a push" in { intercept[IllegalStateException] { fullStack.push(10) } } }