1. 概述
ScalaTest是scala生態系統中最流行和靈活的測試工具,可以測試scala、js、java代碼。
2. ScalaTest的特性
a. ScalaTest的核心是套件(suite),即0到多個測試的集合
b. 測試可以是含有一個名稱的任意內容,該名稱可以用來啟動、待處理或取消,也可表示成功或失敗等
c. trait Suite聲明run和其他"生命周期"的方法,這些方法定義編寫和執行測試的默認方式;"生命周期"方法可被重寫,以定制測試如何編寫和運行
d. ScalaTest提供繼承Suite的樣式traits,並且重寫生命周期方法來支持不同的測試類型。它提供了混合(mixin)特性,重寫生命周期方法,以滿足特定測試需求
e. 你可以通過組合Suite樣式和混合traits來定義測試類;可以通過組合Suite實例來定義測試套件。
3. Maven依賴
Maven項目中增加ScalaTest,只需引入如下依賴即可
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<version>3.0.8</version>
<scope>test</scope>
</dependency>
4. 選取測試樣式的不成文約定
a. 推薦為每個項目選擇一組滿足團隊的測試樣式,同時保持項目代碼的一致性
b. 推薦為單元測試選擇一個主樣式,而驗收測試選擇另一樣式
c. 一般情況下,推薦使用FlatSpec樣式用於單元測試和集成測試,FeatureSpec用於驗收測試。
注意:選擇的樣式只是表明測試聲明的外觀,無論選擇哪種樣式,ScalaTest中的其他內容,均以相同的方式工作
5. 樣式Trait
(1) FunSuite和XUnit類似,可以輕松編寫描述性測試名稱,自然地編寫集中測試,並生成類似規范輸出,促進相關利益者溝通。
(2) FlatSpec的結構類似於XUnit,但是測試名稱必須寫成規定樣式,如X should Y, A must be等
(3) FunSpec類似於Ruby的RSpec工具,對於偏好BDD的團隊來說,FunSpec的嵌套和溫和的結構化文本指南(使用describe和it)為編寫規范式測試提供了極好的通用選擇。
(4) 對於來資源specs或spec2的團隊,WordSpec將會感覺很熟悉。WordSpec在如何編寫文本方面非常規范,因此非常適合於希望在規范文本上強制執行高度管理的團隊。
(5) FreeSpec在如何編寫規范文檔中方面相對自由
(6) PropSpec適合想要在屬性檢查方面專門編寫測試的團隊,當選擇不同的樣式特征作為主要單元測試樣式時,也是編寫偶爾測試矩陣的好選擇
(7) FeatureSpec主要用於驗收測試,包括促進程序員與非程序員一起工作以確定驗收要求的過程
(8) RefSpec允許將測試類定義為方法,與將測試表示為函數的樣式類相比,每個測試保存一個函數文字。更少的函數文字轉換為更快的編譯時間和更少的生成的類文件,這可以幫助最小化構建時間。
因此,在構建時間受到關注的大型項目中以及通過靜態代碼生成器以編程方式生成大量測試時,使用Spec可能是一個不錯的選擇。
6. 定義基類
(1) 為工程創建你最常使用混合特性的抽象基類,而非重復的復制代碼來混合相同的trait,如:
a. 創建抽象基類
import org.scalatest._
abstract class UnitSpec extends FlatSpec with Matchers with OptionValues with Inside with Inspectors
b. 繼承抽象類
import org.scalatest._
class MySpec extends UnitSpec{
// 測試類
}
7. 編寫第一個測試
(1) 使用ScalaTest時,定義一個類,且繼承一個樣式類如FlatSpec
(2) 在FlatSpec中的每個測試由句子構成,該句子指定了一些所需行為和一個測試它的代碼塊。
a. 該句子需要一個主題,如" A Stack"
b. 一個動詞,如should, must, can
示例:"A Stack" should "pop values in last-in-first-out order"
c. 如果多個測試的主題相同,可以使用it來指代之前的主題,如:
it should "throw NoSuchElementException if an empty stack is popped"
d. 句子后面需要增加"in",其后緊跟着以{}括起來的測試代碼示例:

package com.ws.spark.study.scalatest import org.scalatest.FlatSpec import scala.collection.mutable class StackSpec extends FlatSpec{ "A Stack" should "pop values in last-in-first-out order" in { val stack = new mutable.Stack[Int] stack.push(1) stack.push(2) assert(stack.pop() == 2) assert(stack.pop() == 1) } // it 表示之前的主題 "A Stack", 適用於主題相同的情況 it should "throw NoSuchElementException if an empty stack is popped" in { val emptyStack = new mutable.Stack[Int] assertThrows[NoSuchElementException]{ emptyStack.pop() } } }
8. 使用斷言
(0) 參考:

package com.ws.spark.study.scalatest import org.scalatest.FlatSpec import scala.collection.mutable class AssertionTest extends FlatSpec{ /********************************* assert macro *************************************/ /**************************************************/ val left = 2 val right = 1 // 將會打印" 2 did not equal 1 "錯誤 // assert(left == right) /**************************************************/ val a = 1 val b = 2 val c = 3 val d = 4 val xs = List(a, b, c) val num = 1.0 // 打印"1 did not equal 2, and 3 was not greater than or equal to 4" // assert(a == b || c >=d ) // List(1, 2, 3) did not contain 4 // assert(xs.contains(4)) // "help" started with "h", but "goodbye" did not end with "y" // assert("help".startsWith("h") && "goodbye".endsWith("y")) // 1.0 was not instance of scala.Int // assert(num.isInstanceOf[Int]) // Some(2) was not empty // assert(Some(2).isEmpty) /**************************************************/ // 對於不認識的表達式,assert打印string類型信息,並且增加"was false" // scala.None.isDefined was false // assert(None.isDefined) // AssertsionTest.this.xs.exists(((i: Int) => i.>(10))) was false // assert(xs.exists(i => i > 10)) /**************************************************/ val attempted = 2 // assert(attempted == 1, "Execution was attempted " + left + " times instead of 1 time") /********************************* Expected results *************************************/ val x = 5 val y = 2 // Expected 2, but got 3 // assertResult(2) { // x - y // } /********************************* Forcing failures *************************************/ // fail() // fail("I've got a bad feeling about this") /********************************* Achieving success *************************************/ succeed /********************************* Expected exceptions *************************************/ val s = "hi" // 當charAt拋出索引越界異常,assertThrows將返回Succeed // 當charAt正常結束或返回另一個異常時,assertThrows將會立即結束,且拋出TestFailedException assertThrows[IndexOutOfBoundsException] { s.charAt(-1) // Result type: Assertion } val caught = intercept[IndexOutOfBoundsException] { s.charAt(-1) // Result Type: IndexOutOfBoundsException } // caught.getMessage: String index out of range: -1 assert(caught.getMessage.indexOf("-1") != -1) /********************************* Checking that a snippet of code does or does not compile *************************************/ assertDoesNotCompile("val a: String = 1") assertTypeError("val s: String = 1") assertCompiles("val a: Int = 1") /********************************* Assumptions *************************************/ val m = 10 val n = 10 assume(m === n, ", m must equal n") /********************************* Forcing cancelations *************************************/ // cancel() // cancel("Can't run the test because no internet connection was found") /********************************* Forcing cancelations *************************************/ withClue("this is a clue"){ assertThrows[IndexOutOfBoundsException] { "hi".charAt(-1) } assertThrows[NoSuchElementException]{ val stack = new mutable.Stack[Int] stack.pop() } } }
(1) ScalaTest在任意類型的trait中默認均有三種斷言:
a. assert: 通用斷言
b. assertResult: 區分預期值與實際值
c. assertThrows: 確保代碼塊拋出期望異常
(2) ScalaTest中的斷言在trait Assertions中定義,也提供了如下方法:
a. assume: 有條件的取消測試
b. fail: 無條件測試失敗
c. cancel: 無條件取消測試
d. succeed: 無條件測試成功
e. intercept: 確保代碼塊拋出期望異常,然后對異常進行斷言
f. assertDoesNotCompile: 確保代碼塊不編譯
g. assertCompiles: 確保代碼編譯
h. assertTypeError: 確保代碼由於類型錯誤而不被編譯
i. withClue: 當失敗時增加更多信息
(3) assert宏(macro)
a. assert宏的工作原理是識別傳遞給assert的表達式AST中的模式。對於有限的公共表達式集,給出一條錯誤信息,等效於ScalaTest的matcher表達式提供的信息。
b. 對於無法識別的表達式,則會打印string信息,並且在其后增加"was false"
c. 也可以增加錯誤額外信息,通過在assert方法的第二個參數增加
(4) 期望值
a. 通過assertResult實現期望值的斷言,使用方法為assertResult的參數位置為期望值,其后跟着花括號包裹的代碼,代碼執行結果為指定的期望值
(5) 強制失敗
a. 如果想測試失敗,可以直接寫fail(),也可以將錯誤信息增加到fail參數中
(6) 成功實現
a. succeed可以用於異步測試中沒有以Future[Assertion]或Assertion結尾的類型錯誤
(7) 期望異常
a. ScalaTest提供了兩個方法來檢測方法拋出的指定異常:assertThrows和intercept。intercept與assertThrows類似,不同之處在於intercept不返回Succeed類型,而是返回捕獲的異常
(8) 檢測代碼塊是否編譯
a. 當創建庫時期望某些潛在錯誤不被編譯,降低庫的錯誤性,可以使用assertDoesNotCompile
b. 當且僅當類型錯誤時,為了確保代碼塊不被編譯,可以使用assertTypeError。而語法錯誤時,仍將拋出TestFailedException
c. 為了確保代碼塊必須編譯,可以使用assertCompiles
(9) 假設
a. 在測試前使用assume方法,當不滿足條件時,測試將會退出
b. 當不滿足條件時,assume拋出TestCanceledException, 而assert拋出
c. assume方法也自帶參數,可以在異常時增加額外信息
(10) 強制取消
a. cancel方法和fail方法類似,不同在於cancel拋出TestCanceledException,而fail拋出TestFailedException
(11) 獲取線索
a. assert和assertResult自帶線索參數, 而intercept沒有
b. 如若在調用assertThrows拋出的失敗異常的詳細信息中,獲取相同的線索,需要使用withClue
c. withClue方法僅在混合ModifiableMessages trait的異常類型詳情中附加線索信息。
9. "標記"測試
(1) ScalaTest允許定義任意測試類別,將測試"標記"為屬於這些類別,並基於該標記過濾要運行的測試(如有的測試執行時間較長)
(2) ScalaTest默認支持ignore標記,可以"短暫"使一個測試不執行。在FlatSpec樣式中,可以將it或in替代為ignore
(3) ScalaTest支持自定義"標記"測試,在FlatSpec中,可以在in之前將抽象類org.scalatest.tag擴展到taggedas的對象傳遞給它。類tag接收一個string參數,作為名稱
10. 測試裝置
(1) 當多個測試共用相同的裝置(如文件, 套接字, 數據庫連接等),需要避免測試中的重復裝置代碼。
(2) ScalaTest推薦了3種減少重復代碼的方法:
1) 當不同的測試需要不同的裝置時,使用Scala重構
a. get-fixture-methods: 重構extract方法可以為你在每個所需測試中提供可變裝置對象的新實例,但結束時不會清理
參考:com.ws.spark.study.scalatest.fixtures.GetFixtureTest
b. fixture-context-objects: 將裝置的方法和屬性放置到trait中,可以將trait混合,進而為每個測試提供所需的新創建的trait。適用於不同測試中需要可變裝置對象的不同組合,且結束時不需要清理
參考:com.ws.spark.study.scalatest.fixtures.FxiturContextTest
c. loan-fixture-methods: 當不同測試需要不同裝置,且必須結束時進行清理,可通過loan模式重構重復代碼
2) 當大部分測試需要相同裝置時,重載withFixture【推薦使用】
a. withFixture(NoArgTest):
該方法允許在大部分測試的開頭與結尾執行邊緣檢測、轉換測試結果、重試測試、基於測試名稱,標記,或測試數據來做決策。
該方法不適於:
* 不同測試需要不同的裝置 => 可使用scala重構
* 裝置代碼中的異常應終止suite,而非測試失敗 => 可使用before-and-after
* 有需要傳遞到測試中的對象 => 可重載withFixture(OneArgTest)
b. withFixture(OneArgTest): 使用於當需要將相同裝置對象作為參數傳入大部分測試的場景
3). 當代碼失敗時,想要中止suite,而非測試失敗,可以混合before-and-after trait
a. BeforeAndAfter: 當想要在測試之前和之后執行相同邊緣檢測,而非在測試開始和結束時,使用此模板
b. BeforeAndAfterEach:在測試之前和之后堆疊trait
注意:
繼承BeforeAndAfterEach的stacking traits與實現withFixture的traits的不同之處在於:
a. BeforeAndAfterEach中,初始化與清理代碼發生在測試的前后。beforeEach或afterEach異常終止時,將被當做一個SuiteAborted事件
b. withFixture中,初始化和清理代碼發生在測試的開始與結束。withFixture異常終止時,將被當做測試失敗。
11. 共享測試
(1) 有時需要在不同的fixture對象執行相同的測試,在FlatSpec中,首先將需共享的測試放在行為函數中,這些行為函數將在任何FlatSpec
的構建階段調用,因此它們所包含的測試將被注冊為FlatSpec中的測試。
(2) 注意:當使用共享測試時,suite中的每個測試必須有不同的名字。如果在相同的suite中注冊了同名的測試,運行時將會報多個測試注冊到同名的測試異常。
在FlatSpec中,較好的解決方法是確保行為函數的每個調用都有一個不同的主題。
例如: "A Stack (when empty)" should "be empty" in { assert(emptyStack.empty) }
如果“should be empty”測試被分解為行為函數,那么只要行為函數的每次調用都在不同主題的上下文中,就可以重復調用它。
參考:

package com.ws.spark.study.scalatest.sharetests import org.scalatest.FlatSpec import scala.collection.mutable.ListBuffer class Stack[T] { val MAX = 10 private val buf = new ListBuffer[T] def full: Boolean = buf.size == MAX def empty: Boolean = buf.isEmpty def size: Int = buf.size def push(o: T): Unit ={ if(!full) buf.prepend(o) else throw new IllegalStateException("can't push onto a full stack") } def pop(): T ={ if(!empty) buf.remove(0) else throw new IllegalStateException("can't pop an empty stack") } def peek: T = { if(!empty) buf.head else throw new IllegalStateException("can't pop an empty stack") } override def toString: String = buf.mkString("Stack(", ", ", ")") } /** * 1. 有了共享變量,你可以將不同的測試分解為一個行為函數,行為函數中傳入stack fixture,進而可以在測試運行中使用。因此在 * 如下有關Stack的FlatSpec中,將調用行為函數多次 * * 2. 你可以定義一個行為函數,將這些共享測試封裝在使用它們的FlatSpec中。但如果想要在不同的FlatSpec之間共享,也可以在混合 * 到使用它們的每個FlatSpec中的單獨trait定義它們 */ trait StackBehaviors{ this: FlatSpec => def nonEmptyStack(newStack: => Stack[Int], lastItemAdded: Int): Unit ={ it should "be non-empty" in { assert(!newStack.empty) } it should "return the top item on peek" in { assert(newStack.peek === lastItemAdded) } it should "not remove the top item on peek" in { val stack = newStack val size = stack.size assert(stack.peek === lastItemAdded) assert(stack.size === size) } it should "remove the top item on pop" in { val stack = newStack val size = stack.size assert(stack.pop === lastItemAdded) assert(size === stack.size + 1) } } def nonFullStack(newStack: => Stack[Int]): Unit ={ it should "not be full" in { assert(!newStack.full) } it should "add to the top on push" in { val stack = newStack val size = stack.size stack.push(7) assert(stack.size === size + 1) assert(stack.peek === 7) } } } /** * 1. 給定了如上行為函數,你可以直接調用它們,FlatSpec提供一個DSL,類似於: * it should behave like nonEmptyStack(stackWithOneItem, lastValuePushed) * it should behave like nonFullStack(stackWithOneItem) * * 2. 【不推薦】 * 如果傾向於使用命令行樣式更改fixtures,例如混合BeforeAndAfterEach,並在beforeEach重新設置stack變量,則你可以在變量var * 的上下文中編寫行為函數。此時,不需要傳入stack fixture,因為stack fixture已經在行為函數中的范圍。代碼如下: * it should behave like nonEmptyStack // assuming lastValuePushed is also in scope inside nonEmptyStack * it should behave like nonFullStack */ class SharedTestSpec extends FlatSpec with StackBehaviors{ // stack fixture創建方法 def emptyStack = new Stack[Int] def fullStack: Stack[Int] = { val stack = new Stack[Int] for(i <- 0 until stack.MAX){ stack.push(i) } stack } def stackWithOneItem: Stack[Int] = { val stack = new Stack[Int] stack.push(9) stack } def stackWithOneItemLessThanCapacity: Stack[Int] = { val stack = new Stack[Int] for(i <- 1 to 9){ stack.push(i) } stack } val lastValuePushed = 9 "A Stack (when empty)" should "be empty" in { assert(emptyStack.empty) } it should "complain on peek" in { intercept[IllegalStateException] { emptyStack.peek } } it should "complain on pop" in { intercept[IllegalStateException]{ emptyStack.pop() } } "A Stack (with one item)" should behave like nonEmptyStack(stackWithOneItem, lastValuePushed) it should behave like nonFullStack(stackWithOneItem) "A Stack (with one item less than capacity)" should behave like nonEmptyStack(stackWithOneItemLessThanCapacity, lastValuePushed) it should behave like nonFullStack(stackWithOneItemLessThanCapacity) "A Stack (full)" should "be full" in { assert(fullStack.full) } it should behave like nonEmptyStack(fullStack, lastValuePushed) it should "complain on a push" in { intercept[IllegalStateException] { fullStack.push(10) } } }