Simple Scala: Testing

依赖

libraryDependencies ++= Seq("org.specs2" %% "specs2-core" % "3.7.2" % "test")

scalacOptions in Test ++= Seq("-Yrangepos")

一个简单的实例

编写一个测试用例:

import org.specs2._

class QuickStartSpec extends Specification { def is = s2"""

 This is my first specification
   it is working                 $ok
   really working!               $ok
                                 """
}

然后在sbt中运行该用例:

sbt> testOnly QuickStartSpec

Structure

Styles

在测试说明中一般需要包含两件事:

  1. 一些非正式的文本,用于描述系统,应用或函数要完成的工作
  2. 一些明确指定了输入值和预期输出结果的Scala代码

在specs2中有两种主要的方式来完成以上工作:

  1. 可以创建一个Acceptance说明,把说有的非正式文本放在一个地方,代码写在另一个地方.Acceptance的意思是让一些非开发者更易于理解这些说明文本来验证测试.

  2. 可以创建一个Unit说明,代码和文本交叉编写在一起,unit的意思是这样的说明有一个结果,类似于经典单元测试框架Junit.

两种编写说明的方法都是有利有弊的:

  1. 认可说明和故事一样易读,但是需要在代码和文本之间做导航,同时需要定义一个is方法来包含整个说明的实体.
  2. 单元说明更易于操纵但是文本可能在代码的海洋里丢失.

Acceptance specification

需要继承org.specs2.Specification并定义一个is方法,可以通过一个s2类型的字符串来实现这个方法:

class MySpecification extends org.specs2.Specification { def is = s2"""

 this is my specification
   where example 1 must be true           $e1
   where example 2 must be true           $e2
                                          """
  def e1 = 1 must_== 1
  def e2 = 2 must_== 2
}

这个s2字符串包含了说明,同时引用了e1e2两个方法来定义结果.当用例执行时,这个s2字符串会被分析,同时会创建两个Example会被创建并执行:

  1. 第一个Example: 描述文本where example 1 must be true和代码1 must_== 1
    2, 第二个Example: 描述文本where example 2 must be true和代码2 must_== 2

this is my specification会被分析为一个Text而不会执行.

Unit specification

继承自org.specs2.mutable.Specification,使用>>操作符来创建包含ExamplesTexts的块:

class MySpecification extends org.specs2.mutable.Specification {
  "this is my specification" >> {
    "where example 1 must be true" >> {
      1 must_== 1
    }
    "where example 2 must be true" >> {
      2 must_== 2
    }
  }
}

这个用例创建了一个Text和两个Example:

  1. 不再需要定义一个is方法
  2. 代码和它指定的文本描述离的更近

然而一旦一个用例穿件了所有TextsExamples,执行器都会是一样的,无论是Acceptance还是Unit.

>>操作符构成的块可以是嵌套的,这样就可以组织自己的用例机构,比如外部用来描述一个通用的上下文,内部来描述一个特殊的上下文.

Expectations

两种编写方式还有一个不同,第一种风格鼓励每个预期提供一个样例,而第二种风格可以提供多个样例.当测试用例失败时,一个预期一个样例是很有帮助的,可以直接知道是哪里出错了.但是有时候为一个样例创建数据的成本很高,这时,多个预期结果使用一份数据的创建会比较合适.

对于两种风格,如果感觉默认的预期模式不合适的话,可以精确的进行制定.

Functional expectations

acceptance中,默认情况下,ExampleResult总是在用例体的最后一个语句提供.下面的实例中,用例永远不会失败,因为第一个预期已经lost:

// this will never fail!
s2"""
  my example on strings $e1
"""
  def e1 = {
    // because this expectation will not be returned,...
    "hello" must have size (10000)
    "hello" must startWith("hell")
  }

如果同时需要两个预期,可以使用both将他们连接:

s2"""
  my example on strings $e1
"""
  def e1 = ("hello" must have size (10000)) and
           ("hello" must startWith("hell"))

这种用法给人的体验不是很好,因此这也就是这种风格鼓励每个样例一个预期的原因.

Thrown expectations

在单元式说明中默认会得到一个thrown expectations,当一个预期失败时它会抛出一个异常,后续的用例也不会再执行.

class MySpecification extends org.specs2.mutable.Specification {
  "This is my example" >> {
    1 must_== 2 // this fails
    1 must_== 1 // this is not executed
  }
}

Matchers

使用specs2测试预期行为最常用的方式是使用matchers,通常是执行一个动作,命令或函数,然后检查得到的结果与预期是否一致.

比如,利用路径为一个对象创建一个测试用例:

// describe the functionality
s2"the directoryPath method should return well-formed paths $e1"

// give an example with some code
def e1 = Paths.directoryPath("/tmp/path/to/dir") must beEqualTo("/tmp/path/to/dir/")

must操作符使用directoryPath返回的结果应用到由预期值构建的Matcher上.beEqualTo是一种specs2中的Matcher,仅仅用于判断值是否相等.

Equality

匹配器常用的类型beEqualTo用于测试两个值的相等.可以使用多种不同的语法:

Matcher Comment
1 must beEqualTo(1) the normal way
1 must be_==(1) with a symbol
1 must_== 1 my favorite!
1 mustEqual 1 if you dislike underscores
1 should_== 1 for should lovers
1 === 1 the ultimate shortcut
1 must be equalTo(1) with a literate style

还有一些别的比较类型:

Matcher Comment
beTypedEqualTo 类型相等性比较
be_=== beTypedEqualTo一样
a ==== b a must beTypedEqualTo(b)一样
a must_=== b a must_== b一样,但是当ab类型不同时不进行类型检查
be_==~ 当有一个从A到B的隐式类型转换时,检查是否(a: A) == conv(b: B)
beTheSameAs 引用相等,a eq ba must be(b)
be a must be(b),beTheSameAs的同义词
beTrue, beFalse 布尔类型检查

注意: beEqualTo使用的是scala中的==.但是在比较Array时,Scala的==仅仅比较的是引用相等性,eq.因此在使用beEqualTo来比较Array时使用.deep方法比较好,在比较相等性之前将它转换成IndexedSeqs(允许嵌套),因此会得到Array(1, 2, 3) === Array(1, 2, 3),尽管事实上Array(1, 2, 3) != Array(1, 2, 3)(比较引用的方式).

所有Matcher

字符串

Matcher Description
beMatching or be matching 检查字符串是否匹配正则表达式
=~(s) `beMatching(“(. \s)*”+s+”(. \s)*”)`的快捷方式
find(exp).withGroups(a, b, c) 检查字符串是否能找到一些group
have length 检查字符串长度
have size 检查字符串大小,作为Iterable[Char]
be empty 检查是否为空
beEqualTo(b).ignoreCase 排除一些情况后检查两个字符串相等
beEqualTo(b).ignoreSpace 执行replaceAll("\\s", "")后比较相等
beEqualTo(b).trimmed 截除两端后比较是否相等
beEqualTo(b).ignoreSpace.ignoreCase 组合比较
contain(b) 检查是否包含另一个
startWith(b) 检查是否已另一个起始
endWith(b) 检查是否已另一个结尾

Traversable

可遍历对象能够使用多种匹配器,比如检查他们的长度:

  1. 检查非空:

    Seq() must be empty
    Seq(1, 2, 3) must not be empty
    
  2. 检查长度:

    Seq(1, 2) must have size(2)
    Seq(1, 2) must have length(2)           // equivalent to size
    Seq(1, 2) must have size(be_>=(1))      // with a matcher
    

    注意: 在使用一些连接符时需要给haveSize添加注解,比如: (futures: Future[Seq[Int]]) must haveSize[Seq[Int]](1).await

  3. 检查顺序,适用于所有拥有Ordering的类型T:

    Seq(1, 2, 3) must beSorted
    

分别检查各个元素

可以检查包含在可比案例对象中的元素.

  1. 是否包含一个元素: Seq(1, 2, 3) must contain(2)

  2. 是否包含一个匹配指定匹配器的元素: Seq(1, 2, 3) must contain(be_>=(2))

  3. 是否包含通过将值传递给函数返回的Result: Seq(1, 2, 3) must contain((i: Int) => i must be_>=(2))

  4. 注意Seq[A]也是一个Int => A函数,检查一个序列是否包含另一个序列: Seq(Seq(1)) must contain(===(Seq(1)))

  5. 有连个专门的匹配器用于检查元素的字符串表达式:

    Seq(1234, 6237) must containMatch("23")         // matches with ".*23.*"
    Seq(1234, 6234) must containPattern(".*234")    // matches with !.*234"
    

上面的所有检查都可以指定需要满足的次数:

Seq(1, 2, 3) must contain(be_>(0)).forall           // this will stop after the first failure
Seq(1, 2, 3) must contain(be_>(0)).foreach          // this will report all failures
Seq(1, 2, 3) must contain(be_>(0)).atLeastOnce
Seq(1, 2, 3) must contain(be_>(2)).atMostOnce
Seq(1, 2, 3) must contain(be_>(2)).exactly(1.times)
Seq(1, 2, 3) must contain(be_>(2)).exactly(1)
Seq(1, 2, 3) must contain(be_>(1)).between(1.times, 2.times)
Seq(1, 2, 3) must contain(be_>(1)).between(1, 2)

检查所有元素

另一种类型的检查是将可遍历对象的元素与其他元素(值,匹配器,函数返回一个Result)进行比较.

  1. 一组值:

    Seq(1, 2, 3, 4) must contain(2, 4)
    

    或:

    Seq(1, 2, 3, 4) must contain(allOf(2, 4))
    
  2. 一组匹配器:

    Seq(1, 2, 3, 4) must contain(allOf(be_>(0), be_>(1)))
    
  3. 检查是否满足顺序:

    Seq(1, 2, 3, 4) must contain(allOf(be_>(0), be_>(1)).inOrder)
    

….