【编码修炼】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。之所以如此,还是源于对成本与收益的考量。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2014-08-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java3y

海量视频资源【网盘直接取】

资源均来源于网络,在自学/开公众号的时候收集而来。如果侵权请联系我,会第一时间删除。

41K15
来自专栏IT技术精选文摘

一场版本升级引发的性能血案的追凶过程

1004
来自专栏生信技能树

TCGA的28篇教程-整理GDC下载的xml格式的临床资料

GDC给出了一系列的用户友好的选择框,你只需要根据条条框框来选择就可以下载到自己想要的数据,而不需要去几百个文件夹里面漫无目的的查找了。 https://gdc...

2292
来自专栏涤生的博客

天池中间件大赛Golang版Service Mesh思路分享

这次天池中间件性能大赛初赛和复赛的成绩都正好是第五名,出乎意料的是作为Golang是这次比赛的“稀缺物种”,这次在前十名中我也是侥幸存活在C大佬和Java大佬的...

1304
来自专栏圣杰的专栏

ABP入门系列(18)—— 使用领域服务

源码路径:Github-LearningMpaAbp 1.引言 自上次更新有一个多月了,发现越往下写,越不知如何去写。特别是当遇到DDD中一些概念术语的时候...

21910
来自专栏大数据挖掘DT机器学习

如何用R语言从网上读取多样格式数据

第一部分:数据信息 生活中,我们面临着各种各样的数据:比如你的成绩单,比如公司的财务报表,比如朋友圈的一些状态,比如微信里的一段语音……我们生活的大数据时代的一...

3165
来自专栏xingoo, 一个梦想做发明家的程序员

C++库大全

基础类 1、 Dinkumware C++ Library 参考站点:http://www.dinkumware.com P.J. Plauger编写的高品质...

2886
来自专栏Kirito的技术分享

给初中级JAVA准备的面试题

笔者作为一个今年刚毕业的初级JAVA,根据群里水友的讨论,也结合自己刚毕业时的一些面经,加上近期一点点在公司面试别人的经验,总结了如下的常见面试问题,适用于初级...

4758
来自专栏大数据挖掘DT机器学习

如何用R语言从网上读取多样格式数据

第一部分:数据信息 生活中,我们面临着各种各样的数据:比如你的成绩单,比如公司的财务报表,比如朋友圈的一些状态,比如微信里的一段语音……我们生活的大数据时代的一...

3667
来自专栏Golang语言社区

go sync.Mutex 设计思想与演化过程 --转

go语言在云计算时代将会如日中天,还抱着.NET不放的人将会被淘汰。学习go语言和.NET完全不一样,它有非常简单的runtime 和 类库。最好的办法就是将整...

3637

扫码关注云+社区