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類:
classArtist(val firstName: String, val lastName: String)
Albumo類:
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
=
new
Album
(
"Thriller"
,
1981
,
new
Artist
(
"Michael"
,
"Jackson"
))
album
.
artist
.
firstName should be
(
"Michael"
)
}
}
}
在終端輸入test-only AlbumTest回車,得到如下輸出:
[
info
]
AlbumTest
:
[
info
]
An
Album
[
info
]
-
can add an
Artist
object
to the album
[
info
]
Run
completed
in
197
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
18
,
2015
9
:
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
=
new
Album
(
"Thriller"
,
1981
,
new
Artist
(
"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 equal(
3
)
- 在ScalaTest中基本不使用
==和!=進行條件斷言
如果上面的代碼寫成:
list
.
size
==
5
這樣寫只會驗證list.size == 5這是表達式是true或者false,並不會進行斷言的驗證,因而不會有TestFailedException異常拋出,測試將繼續運行。
String Macher
String Macher為字符串斷言提供了一些有用的方法,利用這些方法可以判斷一個字符串是否包含另一個字符串、一個字符串以某個字符串開頭或結尾、一個字符串是否能匹配一個正則表達式等。如下面的例子:
val
string
=
"""I fell into a burning ring of fire.
I went down, down, down and the flames went higher"""
string should startWith(
"I fell"
)
string should endWith(
"higher"
)
string should not endWith
"My favorite friend, the end"
string should include(
"down, down, down"
)
string should not include (
"Great balls of fire"
)
string should startWith regex (
"I.fel+"
)
string should endWith regex (
"h.{4}r"
)
string should not endWith regex(
"\\d{5}"
)
string should include regex (
"flames?"
)
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.9
-
0.8
) should be (
0.1
plusOrMinus
.01
)
(
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
=
new
Artist
(
"Garth"
,
"Brooks"
)
val
chrisGaines
=
garthBrooks
garthBrooks should be theSameInstanceAs
(
chrisGaines
)
val
debbieHarry
=
new
Artist
(
"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
60
by
2
)
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 should contain key (
"Sting"
)
map should contain value (
"Led Zeppelin"
)
map should not contain key(
"Brian May"
)
Compound Macher
ScalaTest中的and和or方法可以用來在測試中使用組合的斷言。如下面的例子:
val
redHotChiliPeppers
=
List
(
"Anthony Kiedis"
,
"Flea"
,
"Chad Smith"
,
"Josh Klinghoffer"
)
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
=
new
Album
(
"Blizzard of Ozz"
,
1980
,
new
Artist
(
"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
]
=
new
JArrayList
[
Int
](
20
)
jList
.
add
(
3
);
jList
.
add
(
6
);
jList
.
add
(
9
)
val
emptyJList
:
JList
[
Int
]
=
new
JArrayList
[
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
)
val
string
=
"""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
=
new
Artist
(
"Garth"
,
"Brooks"
)
val
chrisGaines
=
garthBrooks
val
debbieHarry
=
new
Artist
(
"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
]
{
new
Album
(
"The Joy of Listening to Nothing"
,
1980
,
List
())
}
}
}
上面的例子表示在創建Album時這么寫會捕獲一個IllegalArgumentException.而使用了一個List()只是為了說明在創建Album時不創建Artist,實際上代碼在編譯時就會報錯。
evaluating block
evaluating block這種方式將可能拋出異常的代碼放入一個evaluating代碼塊中,使用一個should或must加上一個produce關鍵字來指明異常的類型。如下面的例子:
val
thrownException
=
evaluating
{
new
Album
(
"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這種方式了。
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
=
new
Album
(
"Thriller"
,
1981
,
new
Artist
(
"Michael"
,
"Jackson"
))
info
(
"Test firstName should be Michael"
)
album
.
artist
.
firstName should be
(
"Thriller"
)
}
}
}
運行測試,得到如下輸出結果:
[
info
]
AlbumTest
:
[
info
]
An
Album
[
info
]
-
can add an
Artist
object
to the album
[
info
]
+
Test
firstName should be
Michael
[
info
]
Run
completed
in
231
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
19
,
2015
9
:
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
=
new
Album
(
"Thriller"
,
1981
,
new
Artist
(
"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
]
An
Album
[
info
]
-
can add an
Artist
to the album at construction time
[
info
]
+
Given
The
album
Thriller
by
Michael
Jackson
[
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
and
last
name should be
Michael
Jackson
[
info
]
Run
completed
in
216
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
19
,
2015
9
:
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
]
An
Album
[
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
=
new
Album
(
"Thriller"
,
1981
,
new
Artist
(
"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
]
An
Album
[
info
]-
can add an
Artist
to the album at construction time
(
pending
)
[
info
]
+
Making
sure that
Michael
Jackson
is
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
=
new
Album
(
"Thriller"
,
1981
,
new
Artist
(
"Michael"
,
"Jackson"
))
album
.
artist
.
firstName should be
(
"Michael"
)
}
ignore
(
"can add a Producer to an album at construction time"
)
{
new
Album
(
"Breezin\'"
,
1976
,
new
Artist
(
"George"
,
"Benson"
))
}
}
}
運行測試,將得到如下的輸出:
[
info
]
AlbumSpec
:
[
info
]
An
Album
[
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
=
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"
)
{
}
}
}
上面的例子中,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。
也可以只執行有某個標記的測試:
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
只有標記為construction的測試才會被執行。
這就是FuncSpec這個測試類的用法了,一個測試類繼承了FuncSpec,將使用如下形式的代碼風格寫測試:
import
org
.
scalatest
.
matchers
.
ShouldMatchers
import
org
.
scalatest
.{
Tag
,
GivenWhenThen
,
FunSpec
}
classAextendsFunSpecwithShouldMachers
{
describe
(
"一些描述信息"
)
{
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
{
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
.{
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
{
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
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
)
def
this
(
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
=
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
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
=
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
.
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
=
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語言。
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
=
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等符號、參數的含義在之前的標記里已經分析過了。
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
=
new
Album
(
"Letter from Home"
,
1989
,
new
Band
(
"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
=
new
Band
(
"Beach Boys"
)
val
beachBoysDiscography
=
new
ListBuffer
[
Album
]()
beachBoysDiscography
+=
(
new
Album
(
"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
+=
(
new
Album
(
"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
+=
(
new
Album
(
"Surfin' Safari"
,
1962
,
fixture
.
beachBoys
))
discographyDeBeachBoys
+=
(
new
Album
(
"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
=
new
Album
(
"Letter from Home"
,
1989
,
new
Band
(
"Pat Metheny Group"
))
}
describe
(
"The Letter From Home Album by Pat Metheny"
)
{
it
(
"should get the year 1989 from the album"
)
{
new
AlbumFixture
{
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
with
OneInstancePerTest
{
val
graceJonesDiscography
=
new
ListBuffer
[
Album
]()
graceJonesDiscography
+=
(
new
Album
(
"Portfolio"
,
1977
,
new
Artist
(
"Grace"
,
"Jones"
)))
"Given an initial Grace Jones Discography"
-
{
"when an additional two albums are added, then the discography size should be 3"
in
{
graceJonesDiscography
+=
(
new
Album
(
"Fame"
,
1978
,
new
Artist
(
"Grace"
,
"Jones"
)))
graceJonesDiscography
+=
(
new
Album
(
"Muse"
,
1979
,
new
Artist
(
"Grace"
,
"Jones"
)))
graceJonesDiscography
.
size should be
(
3
)
}
"when one additional album is added, then the discography size should be 2"
in
{
graceJonesDiscography
+=
(
new
Album
(
"Warm Leatherette"
,
1980
,
new
Artist
(
"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
+=
(
new
Album
(
"Nightclubbing"
,
1981
,
new
Artist
(
"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
=
new
ListBuffer
[
Album
]()
before
{
info
(
"Starting to populate the discography"
)
humanLeagueDiscography
+=
(
new
Album
(
"Dare"
,
1981
,
new
Band
(
"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
+=
(
new
Album
(
"Hysteria"
,
1984
,
new
Band
(
"Human League"
)))
humanLeagueDiscography
+=
(
new
Album
(
"Crash"
,
1986
,
new
Band
(
"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
+=
(
new
Album
(
"Romantic"
,
1990
,
new
Band
(
"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