ScalaTest几乎已经成为Scala语言默认的测试框架,而在JVM平台下,无论是否使用Scala进行开发,我认为仍有尝试ScalaTest的必要。这主要源于它提供了多种表达力超强的测试风格,能够满足各种层次的需求包括单元测试、BDD、验收测试、数据驱动测试。
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提供的方法中,我们完全可以自行开发工具或插件,完成对场景描述以及步骤的提取,生成我们需要的文档。
http://www.scalatest.org/user_guide/using_scalatest_with_intellij
修改pom.xml
,添加以下内容
<!--依赖-->
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalactic</groupId>
<artifactId>scalactic_2.12</artifactId>
<version>3.0.5</version>
</dependency>
<!--插件-->
<plugin>
<groupId>org.scalatest</groupId>
<artifactId>scalatest-maven-plugin</artifactId>
<version>1.0</version>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
<junitxml>.</junitxml>
<filereports>WDF TestSuite.txt</filereports>
</configuration>
<executions>
<execution>
<id>test</id>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>
<!--测试代码和文件-->
<testSourceDirectory>${basedir}/src/test/scala</testSourceDirectory>