FunSepc,WordSpec,FeatureSpec,FreeSpec,FlatSpec大同小异,这里以FeatureSpec为例
FeatureSpec是将测试分类为一系列的功能的测试。一个feature是软件的一个简单的功能点。每个功能将有该功能的多个场景(测试用例),每个场景代表一个成功的或者失败的测试案例。场景越多,测试越充分,健壮性越好。
下面的FunSpec
整合了前面说到的Informer
,GivenWhenThen
,pending
,ignore
和tag
。
import org.scalatest.matchers.ShouldMatchers
import org.scalatest.{Tag, GivenWhenThen, FunSpec}
class AlbumSpecAll extends FunSpec with Matchers with GivenWhenThen {
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 = new Album("Thriller", 1981, new Artist("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] An Album
[info] - can add an Artist to the album at construction time (pending)
[info]
+ Given The album Thriller by Michael Jackson
[info]
+ When Artist 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
。
也可以只执行有某个标记的测试: 只有标记为construction
的测试才会被执行。
test-only AlbumSpecAll -- -n construction
[info] AlbumSpecAll:
[info] An Album
[info] - can add an Artist to the album at construction time (pending)
[info] + Given The album Thriller by Michael Jackson
[info] + When Artist 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
在研究WordSpec
之前,先对前面说到的一些基本类进行一些修改:
Act类
class Act
Album类
class Album(val title: String, val year: Int, val acts: Act*)
Band类
class Band(name: String, members: List[Artist]) extends Act
WordSpec
是ScalaTest
提供的另一个测试类,它大量使用了when
、should
、can
这些属于String
的方法。如下面的例子:
import org.scalatest.{Matchers, WordSpec}
class AlbumWordSpec extends WordSpec with Matchers {
"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 {
new Album("Hotel California", 1997,
new Band("The Eagles", List(new Artist("Don", "Henley"),
new Artist("Glenn", "Frey"),
new Artist("Joe", "Walsh"),
new Artist("Randy", "Meisner"),
new Artist("Don", "Felder"))))
}
}
}
}
运行上面例子的测试,将得到如下结果:
[info] AlbumWordSpec:
[info] An Album
[info] when created
[info] - should accept the title, the year, and a Band as a parameter, and be able to read those parameters back
[info] Run completed in 170 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 May 20, 2015 7:39:45 AM
从上面的结果,结合例子中的代码,可以看出在WordSpec
中,一个测试类继承WordSpec
类,使用如下形式的代码风格写测试:
import org.scalatest.{Matchers, WordSpec}
class A extends WordSpec with Matchers {
"一些描述" when {
"一些描述" should {
"一些描述" in {
// 其它代码
}
}
}
}
在一个when
代码块中,可以使用多个should
代码块,同时should
代码块可以不包含在when
代码块中。如下面的例子:
import org.scalatest.{ShouldMatchers, WordSpec}
class AlbumWordSpec extends WordSpec with ShouldMatchers {
"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 {
new Album("Hotel California", 1997,
new Band("The Eagles", List(new Artist("Don", "Henley"),
new Artist("Glenn", "Frey"),
new Artist("Joe", "Walsh"),
new Artist("Randy", "Meisner"),
new Artist("Don", "Felder"))))
}
}
}
"lack of parameters" should {
"throw an IllegalArgumentException if there are no acts when created" in {
intercept[IllegalArgumentException] {
new Album("The Joy of Listening to Nothing", 2000)
}
}
}
}
运行上面的测试,会得到如下的输出:
[info] AlbumWordSpec:
[info] An Album
[info] when created
[info] - should accept the title, the year, and a Band as a parameter, and be able to read those parameters back
[info] lack of parameters
[info] - should throw an IllegalArgumentException if there are no acts when created
[info] Run completed in 173 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 May 20, 2015 7:49:07 AM
从info
信息的缩进可以看出when
和should
的包含关系,以上就是WordSpec
的用法了。
FeatureSpec
可以通过测试的一些特征(feature)
将测试进行分类,而每一个特征(feature)
又包含若干不同的情节(scenario)
。每个特征(feature)
和情节(scenario)
都需要用不同的字符串来描述。如下面的例子:
import org.scalatest.Matchers
import org.scalatest.FeatureSpec
class AlbumFeatureSpec extends FeatureSpec with Matchers {
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
类
class Track(name: String)
Album类
class Album (val title:String, val year:Int, val tracks:Option[List[Track]], val acts:Act*) {
require(acts.size > 0)
def this(title:String, year:Int, acts:Act*) = this (title, year, None, acts:_*)
}
首先来实现第一个feature
代码块,我们希望给它加入下面的一些scenario
:
Album
时提供一个长度为3的List[Track]Album
时提供一个空ListAlbum
时提供一个null
首先对上例中代码的第一个feature
填充三个scenario
,得到如下代码:
class AlbumFeatureSpec extends FeatureSpec with Matchers {
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 = new Band("Depeche Mode", List(
new Artist("Dave", "Gahan"),
new Artist("Martin", "Gore"),
new Artist("Andrew", "Fletcher"),
new Artist("Alan", "Wilder")))
val blackCelebration = new Album("Black Celebration", 1990,
Some(List(new Track("Black Celebration"),
new Track("Fly on the Windscreen"),
new Track("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 = new Band("The Doobie Brothers",
new Artist("Tom", "Johnston"),
new Artist("Patrick", "Simmons"),
new Artist("Tiran", "Porter"),
new Artist("Keith", "Knudsen"),
new Artist("John", "Hartman"))
when("the album is instantiated with the title, the year, none tracks, and the Doobie Brothers")
val album = new Album("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
是一种形式比较自由的测试,先引入一个类:
JukeBox类
class JukeBox(val albums:Option[List[Album]]) {
def readyToPlay = albums.isDefined
}
再来看一个例子:
import org.scalatest.Matchers
import org.scalatest.FreeSpec
class JukeboxFreeSpec extends FreeSpec with Matchers {
"given 3 albums" - {
val badmotorfinger = new Album("Badmotorfinger", 1991, None, new Band("Soundgarden"))
val thaDoggFather = new Album("The Dogg Father", 1996, None, new Artist("Snoop Doggy", "Dogg"))
val satchmoAtPasadena = new Album("Satchmo At Pasadena", 1951, None, new Artist("Louis", "Armstrong"))
"when a juke box is instantiated it should accept some albums" - {
val jukebox = new JukeBox(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 = new JukeBox(None)
"y regresas 'None' cuando llamado" in {
jukebox.albums should be(None)
}
}
}
从上面的例子中,可以看到FreeSpec
的结构是很自由的。描述字符串
加上一个-{ }
的代码块,如果需要使用断言,则使用描述字符串
加上in
。在FreeSpec
中,并不强制使用should
、when
等内容。在FreeSpec
中,使用如下形式的代码风格写测试:
import org.scalatest.Matchers
import org.scalatest.FreeSpec
class A extends FreeSpec with Matchers {
"一些描述" - {
// 一些代码
"一些描述" in {
// 断言
}
}
}
前面我们说到的一些测试结构可能跟之前用过的如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._
class ArtistJUnitSuite extends JUnitSuite {
var artist:Artist = _
@Before
def startUp() {
artist = new Artist("Kenny", "Rogers")
}
@Test
def addOneAlbumAndGetCopy() {
val copyArtist = artist.addAlbum(new Album("Love will turn you around", 1982, artist))
assertEquals(copyArtist.albums.size, 1)
}
@Test
def addTwoAlbumsAndGetCopy() {
val copyArtist = artist
.addAlbum(new Album("Love will turn you around", 1982, artist))
.addAlbum(new Album("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语言。
跟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._
class ArtistTestNGSuite extends TestNGSuite {
@DataProvider(name = "provider")
def provideData = {
val g = new ArrayBuilder.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
等符号、参数的含义在之前的标记
里已经分析过了。