【編碼修煉】ScalaTest的測試風格
ScalaTest幾乎已經成為Scala語言預設的測試框架,而在JVM平臺下,無論是否使用Scala進行開發,我認為仍有嘗試ScalaTest的必要。這主要源於它提供了多種表達力超強的測試風格,能夠滿足各種層次的需求包括單元測試、BDD、驗收測試、資料驅動測試。正如ScalaTest的建立者Bill Venners所說:
A guiding design principle of ScalaTest is that different people on a team should be able look at each others test code and know immediately what’s going on. ScalaTest is designed to make it easy for you to customize your testing tool to meet your current needs, and for the built-in traits at least, make it easy for anyone who comes along later to read and understand your code.
UT與IT的風格選擇
ScalaTest一共提供了七種測試風格,分別為:FunSuite,FlatSpec,FunSpec,WordSpec,FreeSpec,PropSpec和FeatureSpec。這就好像使用相同的原料做成不同美味乃至不同菜系的佳餚,你可以根據自己的口味進行選擇。以我個人的偏好來看,我傾向於選擇FlatSpec或FunSpec(類似Ruby下的RSpec)來編寫單元測試與整合測試。雖然FunSuite的方式要更靈活,而且更符合傳統測試方法的風格,區別僅在於test()方法可以接受一個閉包,但壞處恰恰就是它太靈活了。而FlatSpec和FunSpec則通過提供諸如it、should、describe等方法,來規定書寫測試的一種模式,例如前者明顯的“主-謂-賓”結構,後者清晰的分級式結構,都可以使團隊的測試更加規範。如下是ScalaTest官方網站的提供的FunSuite、FlatSpec和FunSpec的三種風格樣例。
//FunSuite import org.scalatest.FunSuite class SetSuite extends FunSuite { test("An empty Set should have size 0") { assert(Set.empty.size == 0) } test("Invoking head on an empty Set should produce NoSuchElementException") { intercept[NoSuchElementException] { Set.empty.head } } } //FlatSpec import org.scalatest.FlatSpec class SetSpec extends FlatSpec { "An empty Set" should "have size 0" in { assert(Set.empty.size == 0) } it should "produce NoSuchElementException when head is invoked" in { intercept[NoSuchElementException] { Set.empty.head } } } //FunSpec import org.scalatest.FunSpec class SetSpec extends FunSpec { describe("A Set") { describe("when empty") { it("should have size 0") { assert(Set.empty.size == 0) } it("should produce NoSuchElementException when head is invoked") { intercept[NoSuchElementException] { Set.empty.head } } } } }
至於WordSpec和FreeSpec,要麼太複雜,要麼可讀性稍差,要麼慣用法風格有些混雜,個人認為都不是太好的選擇,除非你已經習慣了這種風格。
資料驅動測試風格
JUnit對類似表資料的Fixture準備提供了Parameterized支援,但非常不直觀,而且還需要為測試編寫建構函式,然後定義一個帶有@Parameters標記的靜態方法。TestNG的DataProvider略好,但通過在測試方法上指定DataProvider的方式,仍然不盡如人意。ScalaTest提供的PropSpec充分利用了Scala函式式語言的特性,使得程式碼更簡單,表達性也更強:
import org.scalatest._
import prop._
import scala.collection.immutable._
class SetSpec extends PropSpec with TableDrivenPropertyChecks with Matchers {
val examples =
Table(
"set", BitSet.empty, HashSet.empty[Int], TreeSet.empty[Int]
)
property("an empty Set should have size 0") {
forAll(examples) { set =>
set.size should be(0)
}
}
property("invoking head on an empty set should produce NoSuchElementException") {
forAll(examples) { set =>
a [NoSuchElementException] should be thrownBy { set.head }
}
}
}
驗收測試風格
我們會推薦由PO(或者需求分析人員BA)與測試人員結對編寫驗收測試的業務場景,然後由開發人員和測試人員結對實現該場景。Cocumber、JBehave、Twist乃至Robot、Fitness都可以用於編寫這樣的驗收測試(Fitness與Robot更接近例項化需求的方式)。這些工具有一個特點是業務場景與測試支援程式碼完全是分開的。例如Cucumber將業務場景放到feature檔案中,而將測試支援程式碼放到rb檔案中。JBehave類似。這樣的好處是feature檔案很乾淨,很純粹,與技術實現沒有任何關係,且有利於生成Living Document。然而,這種分離方式在帶來良好可讀性的同時,也帶來維護成本的增加。
ScalaTest在提供類似Feature的驗收測試Spec時,並沒有將業務場景與測試支援程式碼分開,而是採用了混合的方式來表現:
import org.scalatest.{ShouldMatchers, GivenWhenThen, FeatureSpec}
class TVSetTest extends FeatureSpec with GivenWhenThen with ShouldMatchers{
info("As a TV Set owner")
info("I want to be able to turn the TV on and off")
info("So I can watch TV when I want")
info("And save energy when I'm not watching TV")
feature("TV power button") {
scenario("User press power button when TV is off") {
Given("a TV set that is switched off")
val tv = new TVSet
tv.isOn should be (false)
When("The power button is pressed")
tv.pressPowerButton
Then("The TV should switch on")
tv.isOn should be (true)
}
}
}
ScalaTest的FeatureSpec支援常見的Given-When-Then模式。在上面的程式碼段中,info提供了對Feature的基本描述,然後提供了feature與scenario兩個層級。熟悉Cucumber和JBehave的人對此應該不會陌生。測試支援程式碼直接寫在Given、When、Then方法下,因而針對同一個Feature,只產生一個scala檔案。這就意味著測試支援程式碼與自然語言描述是處於同一級的,準確地說,他們其實就屬於同一個測試。開發時,PO(或者需求)與測試可以先編寫FeatureSpec的骨架,即info-feature-scenario以及Given-When-Then部分。一旦編寫好這個FeatureSpec,就可以提交到版本管理庫。當開發人員與需求、測試一起Kick Off要做的Story時,就可以根據這個FeatureSpec進行,然後,要求開發人員在完成Story的實現前,與測試結對完成它的測試實現程式碼。
由於ScalaTest還提供了Tag等功能,我們還可以通過對測試提取基類或者Trait有效地對這些測試進行重用,保證測試程式碼的可維護性。由於只需要維護一個scala,成本會降低許多,也不需要在業務場景和測試支援程式碼之間跳轉,降低維護的難度。唯一的缺點是它天然不支援Living Document。但是我們發現這些自然語言描述實則都集中在FeatureSpec提供的方法中,我們完全可以自行開發工具或外掛,完成對場景描述以及步驟的提取,生成我們需要的文件。
在我之前的Java專案中,我選擇使用了ScalaTest作為驗收測試的框架。考慮到IDE支援尤其是重構等方面的工具支援,以及構建中對測試執行、測試覆蓋率檢查等的支援,目前我並沒有考慮在Java專案的單元測試和整合測試中使用ScalaTest。之所以如此,還是源於對成本與收益的考量。