ScalaTest 是一个开源测试框架,旨在令测试更加高效。其开发者是Bill Venners(Artima主编)。Bill Venners这样描述ScalaTest:
ScalaTest是比JUnit和TestNG更加高阶的测试编写工具,这个Scala应用在JVM上运行,可以测试Scala以及Java代码。除了与JUnit和TestNG的深层集成外,还支持Ant任务,与maven集成,并包括了流行的Java mocking框架JMock、EasyMock以及Mockito的语法增强。通过JUnit集成,ScalaTest可以轻松地在Eclipse、NetBeans以及IntelliJ IDEA等IDE,以及Infinitest等生产工具中使用。
本文将简单介绍如何使用 ScalaTest 测试框架对我们的Scala代码进行测试。
部分例子来自于 《Testing in Scala》 Daniel Hinojosa O’REILLY
阅读本文,最好对下面列出的内容有一些了解:
有了对 SBT 和 Scala 的一些了解作为前置知识,看下面的内容会比较轻松。(部分单词为了表达上的准确并没有翻译成中文)
简单的例子
下面这个例子来自于Testing
in Scala
一书,先来直观的感觉一下ScalaTest
这个测试框架。
例子中用到的类说明:
Artist类:
// Artist类有两个属性 firstNam表示名字 和 lastName姓氏
classArtist(val firstName: String, val lastName: String)
Albumo类:
// Album 有三个属性 title表示专辑名称 year表示发行年份 artist表示专辑作者
classAlbum(val title: String, val year: Int, val artist: Artist)
测试代码如下:
import org.scalatest.{FunSpec,ShouldMatchers}
classAlbumTestextendsFunSpecwithShouldMatchers{
describe("An Album"){
it("can add an Artist object to the album"){
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
album.artist.firstName should be("Michael")
}
}
}
在终端输入test-only AlbumTest
回车,得到如下输出:
[info]AlbumTest:
[info]AnAlbum
[info]- can add an Artistobject to the album
[info]Run completed in197 milliseconds.
[info]Total number of tests run:1
[info]Suites: completed 1, aborted 0
[info]Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info]All tests passed.
[success]Total time:0 s, completed May18,20159:47:05 PM
根据输入信息可以知道通过测试为1个,则说明我们要验证的can add an Artist object to the album
这个操作是可以执行的。
有了直观的认识,下面来详细的说明ScalaTest
这个测试框架。
在SBT项目中使用ScalaTest
本文中讨论的项目都以SBT构建,相关知识可以在SBT官网学习。
要在SBT的项目中使用ScalaTest测试框架,需要在build.sbt
文件中添加ScalaTest
的依赖。
可以使用
libraryDependencies +="org.scalatest"%%"scalatest"%"2.2.4"
让SBT自动寻最合适的编译版本或者使用
libraryDependencies +="org.scalatest"%"scalatest_2.10.4"%"2.2.4"
手动指定编译版本来添加依赖。
这里的区别仅在于%% "scalatest"
和% "scalatest_2.10.4"
,关于区别的详情,请参看SBT官网。
Machers
在前面的测试代码
import org.scalatest.{FunSpec,ShouldMatchers}
classAlbumTestextendsFunSpecwithShouldMatchers{
describe("An Album"){
it("can add an Artist object to the album"){
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
album.artist.firstName should be("Michael")
}
}
}
里,我们作出了一个断言:Thriller这张专辑的作者的firstName是Michael
。可以看到在代码的第7行有一个should
,这就是ScalaTest中的两大类Macher
之一的Should Macher
,另一类是Must Macher
,用must
来表示。
Macher的类型
前面讲到ScalaTest中的Macher
分为两大类--Should Macher
和Must Macher
。它们的区别仅在测试报告中体现,所以在后面不同情形下的Macher
的说明中,只以Should Macher
为例。
Simple Macher
Simple Macher就是在两个值之间使用一个断言,如下例:
val list =2::4::5::Nil
list.size should be(3)
这里使用了Should Macher
,来判断list.size
是否和3
相等。如果list.siz
的大小不为3
,则测试无法通过。
这里有几点需要注意的:
// 这种写法会导致编译错误
list.size should be 3
// 这种写法和 list.size should be(3) 等价
list.size should equal(3)
- 在ScalaTest中基本不使用
==
和!=
进行条件断言
如果上面的代码写成:
list.size ==5
这样写只会验证list.size == 5
这是表达式是true
或者false
,并不会进行断言的验证,因而不会有TestFailedException
异常抛出,测试将继续运行。
String Macher
String Macher为字符串断言提供了一些有用的方法,利用这些方法可以判断一个字符串是否包含另一个字符串、一个字符串以某个字符串开头或结尾、一个字符串是否能匹配一个正则表达式等。如下面的例子:
valstring="""I fell into a burning ring of fire.
I went down, down, down and the flames went higher"""
// 以 "I fell" 字符串开头
string should startWith("I fell")
// 以 "higher" 字符串结尾
string should endWith("higher")
// 不以 "My favorite friend, the end" 字符串结尾
string should not endWith "My favorite friend, the end"
// 包含 "down, down, down" 字符串
string should include("down, down, down")
// 不包含 "Great balls of fire" 字符串
string should not include ("Great balls of fire")
// 以匹配正则表达式 "I.fel+" 的字符串开头
string should startWith regex ("I.fel+")
// 以匹配正则表达式 "h.{4}r" 的字符串结尾
string should endWith regex ("h.{4}r")
// 不以匹配正则表达式 "\\d{5}" 的字符串结尾
string should not endWith regex("\\d{5}")
// 包含匹配正则表达式 "flames?" 的字符串
string should include regex ("flames?")
// 完全匹配正则表达式 "I(.|\n|\S)*higher"
string should fullyMatch regex ("""I(.|\n|\S)*higher""")
Relational Operator Macher
ScalaTest
框架支持关系运算符,如下面的例子:
val answerToLife =42
answerToLife should be <(50)
answerToLife should not be >(50)
answerToLife should be >(3)
answerToLife should be <=(100)
answerToLife should be >=(0)
answerToLife should be ===(42)
answerToLife should not be ===(400)
上面的例子中,要解释的应该只有===
这个运算符了,这个运算符验等它左边的部分是否等于右边的部分。在前面说过,==
只是验证值是否相等并不会验证断言,因此在涉及验证是否相等
时最好使用should be
或should equal
或===
。
Floating-point Macher
浮点数在JVM中实际上是很复杂的,考虑一个算式0.9 - 0.8
,在我们看来结果应该是0.1
,实际上在REPL中执行这个运算,会得到如下的结果:
scala>0.9-0.8
res0:Double=0.06565656565659998
显然,计算结果是有误差的。在ScalaTest
框架中,提供了一个plusOrMinus
方法来给断言提供一个误差允许范围。如下面的例子:
// 允许右边的范围在 0.1 - 0.01 到 0.1 + 0.01 之间
(0.9 - 0.8) should be (0.1 plusOrMinus .01)
// 允许右边的范围在 40 - 0.3 到 40 + 0.3 之间
(0.4 + 0.1) should not be (40.00 plusOrMinus .30)
实际上上面例子的第4行 0.4 + 0.1
在REPL中会输出一个准确的值 0.5
Reference Macher
在Scala中,==
运算符不会验证引用是否相等,要验证引用是否相等,在ScalaTest
中提供了theSameInstanceAs
方法,如下面的例子:
val garthBrooks =newArtist("Garth","Brooks")
val chrisGaines = garthBrooks
garthBrooks should be theSameInstanceAs (chrisGaines)
val debbieHarry =newArtist("Debbie","Harry")
garthBrooks should not be theSameInstanceAs(debbieHarry)
Iterable Macher
对于Scala的可遍历集合类型,ScalaTest
框架提供了多种进行断言的方法。如下面的例子:
List() should be('empty)
8 :: 6 :: 7 :: 5 :: 3 :: 0 :: 9 :: Nil should contain(7)
上面代码的第一行使用了一个Scala的符号
--'empty
,Scala中的符号
是不可变的占位符。
Seq and Traversable Macher
对于Seq
和Traversable
类型的Scala变量,SalaTest
提供了length
和size
这两个Macher来判定它们的大小(长度)。如下面的例子:
(1 to 9) should have length (9)
(20 to 60by2) should have size (21)
实际上要据Scala文档,length
和size
是等价的,使用哪个完全看你的偏好。
Map Macher
而对于Map
类型的Scala变量,ScalaTest
提供了一些特殊的方法,可以用来判断一个key
或者value
是否在Map
中。如下面的例子:
val map =Map("Jimmy Page"->"Led Zeppelin","Sting"->"The Police","Aimee Mann"->"Til\' Tuesday")
// map中应该包含值为 "Sting" 的key
map should contain key ("Sting")
// map中应该包含值为 "Led Zeppelin" 的value
map should contain value ("Led Zeppelin")
// map中应该不包含值为 "Brian May" 的key
map should not contain key("Brian May")
Compound Macher
ScalaTest
中的and
和or
方法可以用来在测试中使用组合的断言。如下面的例子:
val redHotChiliPeppers =List("Anthony Kiedis","Flea","Chad Smith","Josh Klinghoffer")
// redHotChiliPeppers变量中应该包含 "Anthony Kiedis" 不应该包含 "John Frusciante" 和 "Dave Navarro"
redHotChiliPeppers should (contain("Anthony Kiedis") and (not contain ("John Frusciante") or contain("Dave Navarro")))
在使用组合的Macher时,圆括号()
的使用可能会造成一此困扰,下面是一些规则:
and
和or
的断言必须使用圆括号()
包围起来
- 断言的右边必须使用圆括号
()
包围起来
以下面的例子来说明上面两条规则:
// 这会导致编译错误
redHotChiliPeppers should not contain "The Edge" or contain "Kenny G"
// 这也会导致编译错误
redHotChiliPeppers should not (contain "The Edge" or contain "Kenny G")
// 这是正确的写法
redHotChiliPeppers should not (contain ("The Edge") or contain ("Kenny G"))
除了上面的两条规则,还有一点需要注意的:使用组合Macherand
或or
并不是短路
的。换句话说,就是所有的子句都会被验证。如下面的例子:
var total =3
redHotChiliPeppers should not(contain ("The Edge")or contain {total +=6;"Kenny G"})
total should be (9)
如果发生短路
,total should be (9)
这里肯定不能通过,not contain ("The Edge")
已经是true
,则or
运算没必要再运行。但执行完这个测试发现total
的值已经是9
,说明此时并没有发生短路
。
Scala中有一个Option
类型,其值可以为Some
或None
,因此,在Scala中基本不会使用null
来做处理。ScalaTest
是支持Java的,因此在有些情况下需要用到null
。如下面的例子:
gorillaz should (not be (null)and contain ("Damon Albarn"))
上面的例子中如果gorillaz
为null
则会抛出NullPointerException
异常。更好的最法是将组合Macher拆开,变成下面的形式:
gorillaz should not be (null)
gorillaz should contain ("Damon Albarn")
经过上面的处理,如果gorillaz
为null
,测试不会通过,但其它的测试不会抛出NullPointerException
异常。
Property Macher
ScalaTest
也提供了一个很不错的方式来验证对象的属性,如下面的例子:
import scala.collection.mutable.WrappedArray
val album =newAlbum("Blizzard of Ozz",1980,newArtist("Ozzy","Osbourne"))
album should have (
'title ("Blizzard of Ozz"),
'year(1980),
'artist (new Artist("Ozzy", "Osbourne"))
)
属性Macher可以将对象的属性取出来,然后对这些属性进行断言。这里将属性取出来实际上是使用了对象的getter
方法,所以需要保证在对象中有getter
方法并且能调用到。
java.util.Collection.machers
ScalaTest
是Java友好的,因而它可以像在Scala集合上一样在Java集合上做断言,下面的例子使用了一些在之前用到的方法。
import java.util.{List=>JList,ArrayList=>JArrayList,Map=>JMap,HashMap=>JHashMap}
val jList:JList[Int]=newJArrayList[Int](20)
jList.add(3); jList.add(6); jList.add(9)
val emptyJList:JList[Int]=newJArrayList[Int]()
emptyJList should be('empty)
jList should have length (3)
jList should have size (3)
jList should contain(6)
jList should not contain (10)
val backupBands: JMap[String, String] = new JHashMap()
backupBands.put("Joan Jett", "Blackhearts")
backupBands.put("Tom Petty", "Heartbreakers")
backupBands should contain key ("Joan Jett")
backupBands should contain value ("Heartbreakers")
backupBands should not contain key("John Lydon")
上面例子中的一些Scala语法在这里就不多说了,可以看到在ScalaTest
在Java集合上的操作和在Scala集合上的操作是一样的。
Must Macher
在前面的一些例子中,都是使用Should Macher
的should
这个关键字,实际上可以把前面的should
都换成must
。如下面的一些例子:
val list =2::4::5::Nil
list.size must be(3)
valstring="""I fell into a burning ring of fire.
I went down, down, down and the flames went higher"""
string must startWith regex ("I.fel+")
string must endWith regex ("h.{4}r")
val answerToLife =42
answerToLife must be <(50)
answerToLife must not be >(50)
val garthBrooks =newArtist("Garth","Brooks")
val chrisGaines = garthBrooks
val debbieHarry =newArtist("Debbie","Harry")
garthBrooks must be theSameInstanceAs (chrisGaines)
(0.9-0.8) must be(0.1 plusOrMinus .01)
List() must be('empty)
1 :: 2 :: 3 :: Nil must contain(3)
(1 to 9).toList must have length (9)
(20 to 60 by 2).toList must have size (21)
val map = Map("Jimmy Page" -> "Led Zeppelin", "Sting" -> "The Police", "Aimee Mann" -> "Til\' Tuesday")
map must contain key ("Sting")
map must contain value ("Led Zeppelin")
map must not contain key("Brian May")
val redHotChiliPeppers = List("Anthony Kiedis", "Flea", "Chad Smith", "Josh Klinghoffer")
redHotChiliPeppers must (contain("Anthony Kiedis") and (not contain ("John Frusciante") or contain("Dave Navarro")))
上面的这些例子只是把之前的例子中的should
换成了must
,这完全是等价的。正如之前说过的,Should Macher
和Must Macher
的不同之处只在测试报告中体现。
异常处理
在ScalaTest
中,有两中方式来验证异常的抛出和捕获。intercept block
和evaluating block
。
intercept block
intercept block
这种方式把任何可能抛出异常的代码放入一个intercept
代码块中,如果代码块没有抛出异常,则测试失败
。如下面的例子:
"An album" should {
"throw an IllegalArgumentException if there are no acts when created"in{
intercept[IllegalArgumentException]{
newAlbum("The Joy of Listening to Nothing",1980,List())
}
}
}
上面的例子表示在创建Album
时这么写会捕获一个IllegalArgumentException
.而使用了一个List()
只是为了说明在创建Album
时不创建Artist
,实际上代码在编译时就会报错。
evaluating block
evaluating block
这种方式将可能抛出异常的代码放入一个evaluating
代码块中,使用一个should
或must
加上一个produce
关键字来指明异常的类型。如下面的例子:
val thrownException = evaluating {newAlbum("The Joy of Listening to Nothing",1980,List())} must produce [IllegalArgumentException]
thrownException.getMessage() must be ("An Artist is required")
intercept block
和evaluating block
其实做的是同样的事情,但是使用evaluating block
方式可以捕获到抛出的异常。如果一次调用可能抛出多个不同的异常,那么这种方法的好处就是可以捕获到异常然后判断出抛出的是哪个异常。如果引起某个异常的原因有多个,如上例中的IllegalArgumentException
可能是Artist
未创建造成的,也可能是year
这个参数不合法造成的。那么在这种情况下要确保异常的信息是An Artist is required
,这就只能使用evaluating block
这种方式了。
Informers
Informer在ScalaTest
中跟debug
语句很相似,但它可以放在任何地方来输出一些跟测试相关的信息。使用Informer是很简单的,只要调用info(String)
这上方法就好了。如下面的例子:
import org.scalatest.{FunSpec,ShouldMatchers}
classAlbumTestextendsFunSpecwithShouldMatchers{
describe("An Album"){
it("can add an Artist object to the album"){
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
info("Test firstName should be Michael")
album.artist.firstName should be("Thriller")
}
}
}
运行测试,得到如下输出结果:
[info]AlbumTest:
[info]AnAlbum
[info]- can add an Artistobject to the album
[info]+Test firstName should be Michael
[info]Run completed in231 milliseconds.
[info]Total number of tests run:1
[info]Suites: completed 1, aborted 0
[info]Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info]All tests passed.
[success]Total time:0 s, completed May19,20159:19:28 PM
可以和简单的例子
中的输出结果进行比较,发现在第4行
多出了一个+ Test firstName should be Michael
,这里就是Informer的输出了,以+
开头。
GivenWhenThen
在了解了Informer之后,GivenWhenThen就比较简单了。实际上,任何一个过程者可以被描述为Given
--When
--Then
。Given
相当于所给的前置条件,When
相当于产生了某个动作或处于某种条件下,Then
表示前面两个条件产生的结果。如下面的例子:
import core.{Artist,Album}
import org.scalatest.{GivenWhenThen,ShouldMatchers,FunSpec}
classAlbumSpecextendsFunSpecwithShouldMatcherswithGivenWhenThen{
describe("An Album"){
it("can add an Artist to the album at construction time"){
Given("The album Thriller by Michael Jackson")
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
When("the album\'s artist is obtained")
val artist = album.artist
Then("the artist obtained should be an instance of Artist")
artist.isInstanceOf[Artist] should be(true)
and("the artist's first name and last name should be Michael Jackson")
artist.firstName should be("Michael")
artist.lastName should be("Jackson")
}
}
运行上面的测试,将产生如下的结果:
[info]AlbumSpec:
[info]AnAlbum
[info]- can add an Artist to the album at construction time
[info]+GivenThe album ThrillerbyMichaelJackson
[info]+When the album's artist is obtained
[info] + Then the artist obtained should be an instance of Artist
[info] + And the artist's first name andlast name should be MichaelJackson
[info]Run completed in216 milliseconds.
[info]Total number of tests run:1
[info]Suites: completed 1, aborted 0
[info]Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info]All tests passed.
[success]Total time:0 s, completed May19,20159:31:47 PM
可以看到Given
、When
、Then
、and
里面的字符串都是以Informer的形式输出的,使用一个and
将测试的内容分开了,加强了可读性。而GivenWhenThen
是一个特质,可以被混入任何的类。GivenWhenThen
使测试变得结构化,使得在测试时可以很好的组织思想。
待测试
待测试(Pending Test)这个思想我觉得在实际中会用的比较多。pending
是一个占位符,可以将尚未实现或定义的测试以pending
来填充。Pending Test实际上就是利用pending
来将测试标记为TODO
的。如下面的例子:
classAlbumSpecextendsFunSpecwithShouldMatcherswithGivenWhenThen{
describe("An Album"){
it("can add an Artist to the album at construction time"){pending}
it("can add opt to not have any artists at construction time"){pending}
}
}
运行测试,得到如下结果:
[info]AlbumSpec:
[info]AnAlbum
[info]- can add an Artist to the album at construction time (pending)
[info]- can add opt to not have any artists at construction time (pending)
可以看到,测试都被标记为了pending
。
我们可以将pending
关键字一直放在测试的最下面,直到一个测试完全的写完。如下面的例子:
classAlbumSpecextendsFunSpecwithShouldMatchers{
describe("An Album"){
it("can add an Artist to the album at construction time"){
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
info("Making sure that Michael Jackson is indeed the artist of Thriller")
pending
}
it("can add opt to not have any artists at construction time"){pending}
}
}
运行测试,如我们所料,将输出下面的结果:
[info]AlbumSpec:
[info]AnAlbum
[info]- can add an Artist to the album at construction time (pending)
[info]+Making sure that MichaelJacksonis indeed the artist of Thriller
[info]- can add opt to not have any artists at construction time (pending)
忽略测试
可能有这么一种情境:某个测试案例,可能由于生产代码被修改而处于一种可有可无
的状态。如果留着,在进行测试的时候浪费执行时间,如果删除又怕在后期还要使用到。此时可以使用ignore
来标记该测试,这样在执行test
指令时将不会运行它,但同时又将它保存下来了。如下面的例子:
import org.scalatest.{FunSpec,ShouldMatchers}
classAlbumTestextendsFunSpecwithShouldMatchers{
describe("An Album"){
it("can add an Artist object to the album"){
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
album.artist.firstName should be("Michael")
}
ignore("can add a Producer to an album at construction time"){
newAlbum("Breezin\'",1976,newArtist("George","Benson"))
//TODO: Figure out the implementation of an album producer
}
}
}
运行测试,将得到如下的输出:
[info]AlbumSpec:
[info]AnAlbum
[info]- can add an Artist to the album at construction time
[info]- can add a Producer to an album at construction time !!! IGNORED !!!
这是因为第二个测试can add a Producer to an album at construction time
中的it
被ignore
给替代了,在运行测试时,它将被忽略。在上面的输出结果中的反映就是在测试名后面加上了!!! IGNORED !!!
。如果要恢复这个测试,只需要将ignore
替换成it
就好了。
标记
标记(Tagging)功能给测试加上标签
,这样就可以分组运行测试了。标记可以在下面这些场景中运用:
- 你想跳过某些很费时的测试
- 某些测试是检查一些相关的功能 需要在一起执行
- 你想给测试分成
单元测试
、综合测试
、验收测试
等分类时
不同的测试接口对标记都有自己的实现,但都是使用字符串来进行分类标记。如下面的例子:
it("can add an Artist to the album at construction time",Tag("construction")){
// 其它代码
}
上面的例子是在FunSpec
接口中的实现,给can add an Artist to the album at construction time
这个测试添加了construction
的标记。
在SBT中运行特定标记的测试也有一些需要注意的地方:
- SBT的
test
命令暂时还不能支持运行指定标签的测试
SBT支持多种测试框架,要使test
命令能够按指定标签执行测试,则需要所有SBT支持的测试框架都支持标签
功能,现在ScalaTest
、Specs2
都支持了标签
,但ScalaCheck
目前并不支持标签功能。
- SBT的
test-only
命令是支持执行指定标签的测试的
可以用下例中的方式使用test-only
命令来运行指定的测试:
test-only AlbumTest---n construction
在待测试类名的后面加上--
再加上n
再加上标签,来指行指定的测试(有多个标签 则需要用双引号"
将标签包围起来)。如果要排除某个标签,将前面说的n
换成l
即可。
Specifications
FunSpec
下面的FunSpec
整合了前面说到的Informer
,GivenWhenThen
,pending
,ignore
和tag
。
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.{Tag,GivenWhenThen,FunSpec}
classAlbumSpecAllextendsFunSpecwithShouldMatcherswithGivenWhenThen{
describe("An Album"){
it("can add an Artist to the album at construction time",Tag("construction")){
given("The album Thriller by Michael Jackson")
val album =newAlbum("Thriller",1981,newArtist("Michael","Jackson"))
when("the artist of the album is obtained")
artist = album.artist
then("the artist should be an instance of Artist")
artist.isInstanceOf[Artist] should be(true)
and("the artist's first name and last name should be Michael Jackson")
artist.firstName should be("Michael")
artist.lastName should be("Jackson")
info("This is still pending, since there may be more to accomplish in this test")
pending
}
ignore("can add a Producer to an album at construction time"){
//TODO: Add some logic to add a producer.
}
}
}
上面的例子中,SlbumSpecAll
继随了类FunSpec
混入特质ShouldMachers
、GivenWhenThen
。在前面提到过,ScalaTest
中有很多形式的测试类,上面例子中的FunSpec
就是其中之一。执行上面的测试,将得到下面的输出:
[info]AlbumSpecAll:
[info]AnAlbum
[info]- can add an Artist to the album at construction time (pending)
[info]
+GivenThe album ThrillerbyMichaelJackson
[info]
+WhenArtist of the album is obtained
[info]
+Then the Artist should be an instance of Artist
[info]
+And the artist's first name and last name should be Michael Jackson
[info]
+ This is still pending, since there may be more to accomplish in this
test
[info] - can add a Producer to an album at construction time !!! IGNORED !!!
[info] Passed: : Total 2, Failed 0, Errors 0, Passed 0, Skipped 2
注意上面的输出结果,包含pending
关键字的测试将被Skip
。
也可以只执行有某个标记的测试:
test-only AlbumSpecAll---n construction
[info]AlbumSpecAll:
[info]AnAlbum
[info]- can add an Artist to the album at construction time (pending)
[info]+GivenThe album ThrillerbyMichaelJackson
[info]+WhenArtist of the album is obtained
[info]+Then the Artist should be an instance of Artist
[info]+And the artist's first name and last name should be Michael Jackson
[info] + This is still pending, since there may be more to accomplish in this test
[info] Passed: : Total 1, Failed 0, Errors 0, Passed 0, Skipped 1
只有标记为construction
的测试才会被执行。
这就是FuncSpec
这个测试类的用法了,一个测试类继承了FuncSpec
,将使用如下形式的代码风格写测试:
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.{Tag,GivenWhenThen,FunSpec}
classAextendsFunSpecwithShouldMachers{
describe("一些描述信息"){
it("一些描述信息"){
}
// 其它的 it
}
}
在断言中,可以使用ShouldMacher
、MustMacher
、Informer
、等特质提供的方法,在describe
代码块中也可以使用GivenWhenThen
特质来使测试更加结构化。
WordSpec
在研究WordSpec
之前,先对前面说到的一些基本类进行一些修改:
Act类
classAct
Album类
classAlbum(val title:String, val year:Int, val acts:Act*)
Band类
classBand(name:String, members:List[Artist])extendsAct
WordSpec
是ScalaTest
提供的另一个测试类,它大量使用了when
、should
、can
这些属于String
的方法。如下面的例子:
import org.scalatest.{ShouldMatchers,WordSpec}
classAlbumWordSpecextendsWordSpecwithShouldMatchers{
"An Album"when{
"created" should {
"accept the title, the year, and a Band as a parameter, and be able to read those parameters back"in{
newAlbum("Hotel California",1997,
newBand("The Eagles",List(newArtist("Don","Henley"),
newArtist("Glenn","Frey"),
newArtist("Joe","Walsh"),
newArtist("Randy","Meisner"),
newArtist("Don","Felder"))))
}
}
}
}
运行上面例子的测试,将得到如下结果:
[info]AlbumWordSpec:
[info]AnAlbum
[info]when created
[info]- should accept the title, the year,and a Bandas a parameter,and be able to read those parameters back
[info]Run completed in170 milliseconds.
[info]Total number of tests run:1
[info]Suites: completed 1, aborted 0
[info]Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0
[info]All tests passed.
[success]Total time:0 s, completed May20,20157:39:45 AM
从上面的结果,结合例子中的代码,可以看出在WordSpec
中,一个测试类继承WordSpec
类,使用如下形式的代码风格写测试:
import org.scalatest.{ShouldMatchers,WordSpec}
classAextendsWordSpecwithShouldMatchers{
"一些描述"when{
"一些描述" should {
"一些描述"in{
// 其它代码
}
}
}
}
在一个when
代码块中,可以使用多个should
代码块,同时should
代码块可以不包含在when
代码块中。如下面的例子:
import org.scalatest.{ShouldMatchers,WordSpec}
classAlbumWordSpecextendsWordSpecwithShouldMatchers{
"An Album"when{
"created" should {
"accept the title, the year, and a Band as a parameter, and be able to read those parameters back"in{
newAlbum("Hotel California",1997,
newBand("The Eagles",List(newArtist("Don","Henley"),
newArtist("Glenn","Frey"),
newArtist("Joe","Walsh"),
newArtist("Randy","Meisner"),
newArtist("Don","Felder"))))
}
}
}
"lack of parameters" should {
"throw an IllegalArgumentException if there are no acts when created"in{
intercept[IllegalArgumentException]{
newAlbum("The Joy of Listening to Nothing",2000)
}
}
}
}
运行上面的测试,会得到如下的输出:
[info]AlbumWordSpec:
[info]AnAlbum
[info]when created
[info]- should accept the title, the year,and a Bandas a parameter,and be able to read those parameters back
[info] lack of parameters
[info]- should throw an IllegalArgumentExceptionif there are no acts when created
[info]Run completed in173 milliseconds.
[info]Total number of tests run:2
[info]Suites: completed 1, aborted 0
[info]Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info]All tests passed.
[success]Total time:0 s, completed May20,20157:49:07 AM
从info
信息的缩进可以看出when
和should
的包含关系,以上就是WordSpec
的用法了。
FeatureSpec
FeatureSpec
可以通过测试的一些特征(feature)
将测试进行分类,而每一个特征(feature)
又包含若干不同的情节(scenario)
。每个特征(feature)
和情节(scenario)
都需要用不同的字符串来描述。如下面的例子:
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FeatureSpec
classAlbumFeatureSpecextendsFeatureSpecwithShouldMatchers{
feature("An album's default constructor should support a parameter that acceptsOption(List(Tracks)) "){...}
feature("An album should have an addTrack method that takes a track and returns an immutable copy of the Album with the added track"){...}
}
上面的例子中,我们定义了一个AlbumFeatureSpec
类,它继承了FeatureSpec
类。在AlbumFeatureSpec
类中,写了两个feature
代码块,但在这两个代码块中并未写任何的scenario
。在继续分析上例代码前,需要再给Album
类添加一些内容。
Track
类
classTrack(name:String)
Album类
classAlbum(val title:String, val year:Int, val tracks:Option[List[Track]], val acts:Act*){
require(acts.size >0)
defthis(title:String, year:Int, acts:Act*)=this(title, year,None, acts:_*)
}
首先来实现第一个feature
代码块,我们希望给它加入下面的一些scenario
:
- 构造
Album
时提供一个长度为3的List[Track]
- 构造
Album
时提供一个空List
- 构造
Album
时提供一个null
首先对上例中代码的第一个feature
填充三个scenario
,得到如下代码:
classAlbumFeatureSpecextendsFeatureSpecwithShouldMatchers{
feature("An album's default constructor should support a parameter that accepts Option(List(Tracks))"){
scenario ("Album's default constructor is given a list of the 3 tracks exactly for the tracks parameter"){pending}
scenario ("Album's default constructor is given an empty List for the tracks parameter"){pending}
scenario ("Album's default constructor is given null for the tracks parameter"){pending}
}
feature("An album should have an addTrack method that takes a track and returns an immutable copy of the Album with the added track"){}
}
接下来要做的就是给这三个scenario
加上实现的代码。
在Album's default constructor is given a list of the 3 tracks exactly for the tracks parameter
这个scenario
中,我们加入如下代码:
val depecheModeCirca1990 =newBand("Depeche Mode",List(
newArtist("Dave","Gahan"),
newArtist("Martin","Gore"),
newArtist("Andrew","Fletcher"),
newArtist("Alan","Wilder")))
val blackCelebration =newAlbum("Black Celebration",1990,
Some(List(newTrack("Black Celebration"),
newTrack("Fly on the Windscreen"),
newTrack("A Question of Lust"))), depecheModeCirca1990)
blackCelebration.tracks.get should have size (3)
接下来是Album's default constructor is given an empty List for the tracks parameter
这个scenario
:
given("the band, the Doobie Brothers from 1973")
val theDoobieBrothersCirca1973 =newBand("The Doobie Brothers",
newArtist("Tom","Johnston"),
newArtist("Patrick","Simmons"),
newArtist("Tiran","Porter"),
newArtist("Keith","Knudsen"),
newArtist("John","Hartman"))
when("the album is instantiated with the title, the year, none tracks, and the Doobie Brothers")
val album =newAlbum("The Captain and Me",1973,None, theDoobieBrothersCirca1973)
then("calling the albums's title, year, tracks, acts property should yield the same results")
album.title should be("The Captain and Me")
album.year should be(1973)
album.tracks should be(None)
album.acts(0) should be(theDoobieBrothersCirca1973)
第三个scenario
我这里就不写了,下面来看一下运行测试的结果:
[info]AlbumFeatureSpec:
[info]Feature:An album's default constructor should support a parameter that accepts Option(List(Tracks))
[info] Scenario: Album's default constructor is given a list of the 3 tracks exactly for the tracks parameter
[info]Scenario:Album's default constructor is given a None for the tracks parameter
[info] Given the band, the Doobie Brothers from 1973
[info] When the album is instantiated with the title, the year, none tracks, and the Doobie Brothers
[info] Then calling the albums's title, year, tracks, acts property should yield the same results
[info]Scenario:Album's default constructor is given null for the tracks parameter (pending)
[info] Feature: An album should have an addTrack method that takes a track and returns an immutable copy of the Album with the added track
[info] Run completed in 177 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 1
[info] All tests passed.
[success] Total time: 0 s, completed May 20, 2015 8:38:47 AM
FeatureSpec
使用特征(feature)
将测试进行分类,每一个特征(feature)
又包含若干不同的情节(scenario)
,对这些情节(scenario)
的实现实际上就是完成测试的过程。
FreeSpec
FreeSpec
是一种形式比较自由的测试,先引入一个类:
JukeBox类
classJukeBox(val albums:Option[List[Album]]){
def readyToPlay = albums.isDefined
}
再来看一个例子:
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FreeSpec
classJukeboxFreeSpecextendsFreeSpecwithShouldMatchers{
"given 3 albums"-{
val badmotorfinger =newAlbum("Badmotorfinger",1991,None,newBand("Soundgarden"))
val thaDoggFather =newAlbum("The Dogg Father",1996,None,newArtist("Snoop Doggy","Dogg"))
val satchmoAtPasadena =newAlbum("Satchmo At Pasadena",1951,None,newArtist("Louis","Armstrong"))
"when a juke box is instantiated it should accept some albums"-{
val jukebox =newJukeBox(Some(List(badmotorfinger, thaDoggFather, satchmoAtPasadena)))
"then a jukebox's album catalog size should be 3"in{
jukebox.albums.get should have size (3)
}
}
}
"El constructor de Jukebox puedo aceptar la palabra clave de 'None'"-{
val jukebox =newJukeBox(None)
"y regresas 'None' cuando llamado"in{
jukebox.albums should be(None)
}
}
}
从上面的例子中,可以看到FreeSpec
的结构是很自由的。描述字符串
加上一个-{ }
的代码块,如果需要使用断言,则使用描述字符串
加上in
。在FreeSpec
中,并不强制使用should
、when
等内容。在FreeSpec
中,使用如下形式的代码风格写测试:
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FreeSpec
classAextendsFreeSpecwithShouldMatchers{
"一些描述"-{
// 一些代码
"一些描述" in {
// 断言
}
}
}
JUnitSuite
前面我们说到的一些测试结构可能跟之前用过的如Junit
、TestNG
这些有较大的差异,如果你比较喜欢像JUnit
、TestNG
这种测试风格,ScalaTest
也是支持的。为了使用这种风格,首先在要build.sbt
文件中添加JUnit
的依赖:
libraryDependencies +="junit"%"junit"%"4.12"
下面来看一个使用ScalaTest
写的JUnit
风格的测试:
import org.scalatest.junit.JUnitSuite
import org.junit.{Test,Before}
import org.junit.Assert._
classArtistJUnitSuiteextendsJUnitSuite{
var artist:Artist= _
@Before
def startUp(){
artist =newArtist("Kenny","Rogers")
}
@Test
def addOneAlbumAndGetCopy(){
val copyArtist = artist.addAlbum(newAlbum("Love will turn you around",1982, artist))
assertEquals(copyArtist.albums.size,1)
}
@Test
def addTwoAlbumsAndGetCopy(){
val copyArtist = artist
.addAlbum(newAlbum("Love will turn you around",1982, artist))
.addAlbum(newAlbum("We've got tonight",1983, artist))
assertEquals(copyArtist.albums.size,2)
}
@After
def shutDown(){
this.artist =null
}
}
上面的例子中startUp
方法被注解Before
标记,addOneAlbumAndGetCopy
方法和addTwoAlbumsAndGetCopy
方法被注解Test
,shutDown
方法被注解After
标记。注解Test
将方法标记为测试方法,而注解Before
将方法标记为每个测试方法执行前执行的方法,注解After
则将方法标记为每个测试方法执行后执行的方法。
因此,addOneAlbumAndGetCopy
方法和addTwoAlbumsAndGetCopy
方法执行前startUp
方法会被调用,而方法执行结束shutDown
方法会被调用。
上面例子的风格跟使用JUnit
来做测试是一样的,只不过我们使用了Scala语言。
TestNGSuit
跟JUnit
类似,在ScalaTest
中也提供了TestNG
风格的测试写法。同样的,需要使用TestNG
风格,要先在build.sbt
中添加TestNG
的依赖:
libraryDependencies +="org.testng"%"testng"%"6.8.21"
我们也会一个例子来说明:
import org.scalatest.testng.TestNGSuite
import collection.mutable.ArrayBuilder
import org.testng.annotations.{Test,DataProvider}
import org.testng.Assert._
classArtistTestNGSuiteextendsTestNGSuite{
@DataProvider(name ="provider")
def provideData ={
val g =newArrayBuilder.ofRef[Array[Object]]()
g +=(Array[Object]("Heart",5.asInstanceOf[java.lang.Integer]))
g +=(Array[Object]("Jimmy Buffet",12.asInstanceOf[java.lang.Integer]))
g.result()
}
@Test(dataProvider ="provider")
def testTheStringLength(n1:String, n2:java.lang.Integer){
assertEquals(n1.length, n2)
}
}
上面的例子中,provideData
方法被注解DataProvider
标记,testTheStringLength
方法被注解Test
标记。注解Test
将方法标记为测试方法,属性dataProvider
指定了测试数据由哪个DataProvider
来提供。注解DataProvider
将一个方法标记为一个DataProvider
。
上面例子中的测试执行,则testTheStringLength
测试法的中的测试数据是来自于provideData
这个方法。
另外一点,在TestNG
中,标签(Tag)功能被称为group
,给一个测试添加group
的写法如下:
@Test(dataProvider ="provider", groups=Array("word_count_analysis"))
def testTheStringLength(n1:String, n2:java.lang.Integer){
assertEquals(n1.length, n2)
}
使用如下命令执行指定group
的测试:
test-only ArtistTestNGSuite---n word_count_analysis
关于上面这条命令中--
、-n
等符号、参数的含义在之前的标记
里已经分析过了。
Fixtures
Fixture翻译成中文有这么些意思:固定装置;卡具;固定附物,固定附着物;固定财产
,在ScalaTest中,可能会有这么一种情境:在一个测试类中,不同的测试方法需要的类实例、依赖等数据是一样的,显然,没必要为每个测试类去new
一些它们专用的数据,可以提供一些公共的数据,然后在不同的测试方法中重用它们。
要做到数据的重用,有很多方法:
- Scala语言自带的方法
ScalaTest
测试框架提供的解决方案
- 每一种测试方法也有自己的一些实现
JUnit
和TestNG
也有它们自己的结构
匿名对象
先从Scala语言本身提供的方案说起。Scala语言提供的匿名对象
可以用来解决前面说到的数据重用的问题。Scala中的匿名对象
就是没有名字的对象,匿名对象
一旦被创建,就可以在不同的测试方法中重用。每次匿名对象
被请求的时候,它都会创建一个全新的对象。如下面的例子:
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FunSpec
classAlbumFixtureSpecextendsFunSpecwithShouldMatchers{
def fixture =new{
val letterFromHome =newAlbum("Letter from Home",1989,newBand("Pat Metheny Group"))
}
describe("The Letter From Home Album by Pat Metheny"){
it("should get the year 1989 from the album"){
val album = fixture.letterFromHome
album.year should be (1989)
}
}
}
上面的例子定义了一个fixture
方法来获取一个Album
对象,fixture
方法每次被调用都会产生一个匿名对象
。
这里有一点需要注意的,即使fixture
方法产生的是一个可变mutable
的对象,在另一个方法调用fixture
时,它仍然后产生一个新的对象,而不是提供之前的对象。下面的例子使用了可变集合来说明:
import org.scalatest.FunSpec
import org.scalatest.matchers.ShouldMatchers
classAlbumMutableFixtureSpecextendsFunSpecwithShouldMatchers{
def fixture =new{
import scala.collection.mutable._
val beachBoys =newBand("Beach Boys")
val beachBoysDiscography =newListBuffer[Album]()
beachBoysDiscography +=(newAlbum("Surfin' Safari",1962, beachBoys))
}
describe("Given a single fixture each beach boy discography initially contains a single album"){
it("then after 1 album is added, the discography size should have 2"){
val discographyDeBeachBoys = fixture.beachBoysDiscography
discographyDeBeachBoys +=(newAlbum("Surfin' Safari",1962, fixture.beachBoys))
discographyDeBeachBoys.size should be(2)
}
it("then after 2 albums are added, the discography size should return 3"){
val discographyDeBeachBoys = fixture.beachBoysDiscography
discographyDeBeachBoys +=(newAlbum("Surfin' Safari",1962, fixture.beachBoys))
discographyDeBeachBoys +=(newAlbum("All Summer Long",1964, fixture.beachBoys))
discographyDeBeachBoys.size should be(3)
}
}
}
跟前一个例子一样,上面的例子使用了fixture
方法,在Scala语言中,使用def
定义的方法在每次被调用的时候都会重新执行方法体。因而,在每个测试方法中我们得到的都是新的实例。
Fixture Traits
另一种在ScalaTest
中的可供选择的做法是自定义一个特质
来确保每个测试方法都得到不同的对象。特质
在混入对象后仍然后持有它原来的方法,并不会在混入的对象之中共享。下面的例子使用一个特质
而不是一个匿名对象
:
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.FunSpec
classAlbumFixtureTraitSpecextendsFunSpecwithShouldMatchers{
traitAlbumFixture{
val letterFromHome =newAlbum("Letter from Home",1989,newBand("Pat Metheny Group"))
}
describe("The Letter From Home Album by Pat Metheny"){
it("should get the year 1989 from the album"){
newAlbumFixture{
letterFromHome.year should be(1989)
}
}
}
}
上面的例子使用了一个特质来封装测试方法需要的数据,在特质中又使用了匿名对象
的方式来创建对象,实际上,这种实现方式依然使用了Scala的语言特性。
OneInstancePerTest
除了依赖Scala的语言特性,ScalaTest
也提供了方法来确保每个测试都有它自己的数据实例。下面的例子使用了OnInstancePerTest
特质来实现:
import org.scalatest.matchers.ShouldMatchers
import collection.mutable.ListBuffer
import org.scalatest.{FreeSpec,OneInstancePerTest}
classAlbumListOneInstancePerTestFreeSpecextendsFreeSpecwithShouldMatchers
withOneInstancePerTest{
val graceJonesDiscography =newListBuffer[Album]()
graceJonesDiscography +=(newAlbum("Portfolio",1977,newArtist("Grace","Jones")))
"Given an initial Grace Jones Discography"-{
"when an additional two albums are added, then the discography size should be 3"in{
graceJonesDiscography +=(newAlbum("Fame",1978,newArtist("Grace","Jones")))
graceJonesDiscography +=(newAlbum("Muse",1979,newArtist("Grace","Jones")))
graceJonesDiscography.size should be(3)
}
"when one additional album is added, then the discography size should be 2"in{
graceJonesDiscography +=(newAlbum("Warm Leatherette",1980,newArtist("Grace","Jones")))
graceJonesDiscography.size should be(2)
}
}
"Given an initial Grace Jones Discography "-{
"when one additional album from 1980 is added, then the discography size should be 2"in{
graceJonesDiscography +=(newAlbum("Nightclubbing",1981,newArtist("Grace","Jones")))
graceJonesDiscography.size should be(2)
}
}
}
上面的例子使用了FreeSpec
风格的测试写法。在测试开始时,定义了graceJonesDiscography
变量,然后该变量被用在多个测试中。由于AlbumListOneInstancePerTestFreeSpec
类混入了OneInstancePerTest
接口,graceJonesDiscography
变量在每个测试方法中使用时都会被重新创建。
上面的例子中,测试方法是在in
代码块中的内容。
Before and After
为了更好的控制在测试方法执行前、后有什么行为,ScalaTest
提供了一个名为BeforeAndAfter
的特质。可以很方便的指定在每一个测试方法执行前有什么行为,在每个测试方法执行后有什么行为。如下面的例子:
import collection.mutable.ListBuffer
import org.scalatest.{BeforeAndAfter,WordSpec}
import org.scalatest.matchers.ShouldMatchers
classAlbumBeforeAndAfterFixtureSpecextendsWordSpecwithShouldMatcherswithBeforeAndAfter{
val humanLeagueDiscography =newListBuffer[Album]()
before {
info("Starting to populate the discography")
humanLeagueDiscography +=(newAlbum("Dare",1981,newBand("Human League")))
}
"A mutable ListBuffer of albums" should {
"have a size of 3 when two more albums are added to the Human League Discography"in{
humanLeagueDiscography +=(newAlbum("Hysteria",1984,newBand("Human League")))
humanLeagueDiscography +=(newAlbum("Crash",1986,newBand("Human League")))
humanLeagueDiscography should have size (3)
}
"have a size of 2 when one more album is added to the Human League Discography"in{
humanLeagueDiscography +=(newAlbum("Romantic",1990,newBand("Human League")))
humanLeagueDiscography should have size (2)
}
}
after {
info("Clearing the discography")
humanLeagueDiscography.clear()
}
}
上面的例子使用了WordSpec
风格的测试,在测试方法执行前,初始化了一个名为humanLeagueDiscography
的可变列表,在测试方法执行完毕后,humanLeagueDiscography
可变列表被清空。BeforeAndAfter
特质中的before
和after
方法和在JUnit
中被标记为Before
和After
的方法作用是一样的。
上面的例子中,测试方法依然是在in
代码块中的内容。
简单的总结
ScalaTest
是一个强大的测试框架,它是Java友好的,集成了JUnit
和TestNG
的测试风格,是学习Scala语言的一大利器。本文通过各种代码举例,简单的说明了ScalaTest
中的各种Spec
,是对ScalaTest
的一个简单的介绍。软件 测度是一门大学问,只有通过不断的实践才能获得属于自己的经验。
本文中大量参考了《Testing in Scala》一书,文中有些地方是对原书的翻译,本人水平有限,对知识的理解也有限,欢迎大家指点~如果您对本文有任何疑问,欢迎与我联系。
参考资料
Testing in Scala by Daniel Hinojosa (O’REILLY)密码:5vjs