依赖
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
在测试说明中一般需要包含两件事:
- 一些非正式的文本,用于描述系统,应用或函数要完成的工作
- 一些明确指定了输入值和预期输出结果的Scala代码
在specs2中有两种主要的方式来完成以上工作:
可以创建一个
Acceptance说明,把说有的非正式文本放在一个地方,代码写在另一个地方.Acceptance的意思是让一些非开发者更易于理解这些说明文本来验证测试.可以创建一个
Unit说明,代码和文本交叉编写在一起,unit的意思是这样的说明有一个结果,类似于经典单元测试框架Junit.
两种编写说明的方法都是有利有弊的:
- 认可说明和故事一样易读,但是需要在代码和文本之间做导航,同时需要定义一个
is方法来包含整个说明的实体. - 单元说明更易于操纵但是文本可能在代码的海洋里丢失.
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字符串包含了说明,同时引用了e1和e2两个方法来定义结果.当用例执行时,这个s2字符串会被分析,同时会创建两个Example会被创建并执行:
- 第一个
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,使用>>操作符来创建包含Examples和Texts的块:
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:
- 不再需要定义一个
is方法 - 代码和它指定的文本描述离的更近
然而一旦一个用例穿件了所有Texts和Examples,执行器都会是一样的,无论是Acceptance还是Unit.
由>>操作符构成的块可以是嵌套的,这样就可以组织自己的用例机构,比如外部用来描述一个通用的上下文,内部来描述一个特殊的上下文.
Expectations
两种编写方式还有一个不同,第一种风格鼓励每个预期提供一个样例,而第二种风格可以提供多个样例.当测试用例失败时,一个预期一个样例是很有帮助的,可以直接知道是哪里出错了.但是有时候为一个样例创建数据的成本很高,这时,多个预期结果使用一份数据的创建会比较合适.
对于两种风格,如果感觉默认的预期模式不合适的话,可以精确的进行制定.
Functional expectations
在acceptance中,默认情况下,Example的Result总是在用例体的最后一个语句提供.下面的实例中,用例永远不会失败,因为第一个预期已经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 b或a 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
可遍历对象能够使用多种匹配器,比如检查他们的长度:
检查非空:
Seq() must be empty Seq(1, 2, 3) must not be empty检查长度:
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检查顺序,适用于所有拥有
Ordering的类型T:Seq(1, 2, 3) must beSorted
分别检查各个元素
可以检查包含在可比案例对象中的元素.
是否包含一个元素:
Seq(1, 2, 3) must contain(2)是否包含一个匹配指定匹配器的元素:
Seq(1, 2, 3) must contain(be_>=(2))是否包含通过将值传递给函数返回的
Result:Seq(1, 2, 3) must contain((i: Int) => i must be_>=(2))注意
Seq[A]也是一个Int => A函数,检查一个序列是否包含另一个序列:Seq(Seq(1)) must contain(===(Seq(1)))有连个专门的匹配器用于检查元素的字符串表达式:
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)进行比较.
一组值:
Seq(1, 2, 3, 4) must contain(2, 4)或:
Seq(1, 2, 3, 4) must contain(allOf(2, 4))一组匹配器:
Seq(1, 2, 3, 4) must contain(allOf(be_>(0), be_>(1)))检查是否满足顺序:
Seq(1, 2, 3, 4) must contain(allOf(be_>(0), be_>(1)).inOrder)
….