【编码修炼】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 条评论
登录 后参与评论

相关文章

来自专栏ytkah

Unix时间戳转换怎样在Excel批量修改?

  最近在操作项目的时候碰到一个Unix时间戳转换的问题。"date_time":1393031347这个是什么,你知道吗?如果你对Unix时间戳了解的话一眼就...

35712
来自专栏技术分享

.NET应用架构设计—表模块模式与事务脚本模式的代码编写

阅读目录: 1.背景介绍 2.简单介绍表模块模式、事务脚本模式 3.正确的编写表模块模式、事务脚本模式的代码 4.总结 1.背景介绍 要想正确的设计系统架...

19510
来自专栏Java架构师学习

今天是JVM的生日,来了解下JVM的发展历史吧

842
来自专栏Danny的专栏

【软考路上】——操作系统

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/...

602
来自专栏魏琼东

DotNET企业架构应用实践-系统架构与性能-在业务中实例使用缓存与缓存查询-附上视频

回顾与说明      本文是DotNET企业架构应用实践系列中的一篇文章,同时也是一步一步教你使用AgileEAS.NET基础类库进行应用开发系统中的一篇文章,...

2319
来自专栏雪胖纸的玩蛇日常

第一章 介绍与循环

1343
来自专栏微信公众号:Java团长

如何扎实自己的Java基础?

JDK其实就是Java SE Development Kit的缩写,要玩好这东西可不简单。JDK主要包含了三部分,第一部分就是Java运行时环境,这其实就是JV...

683
来自专栏猿天地

Spring Cloud如何提供API给客户端

现在越来越多的公司开始拥抱Spring Cloud了,很多Java方向的同学也开始积极的学习Spring Cloud,其实这边还有一个问题就是说:虽然大家学了E...

3377
来自专栏ImportSource

Java9里将会出现的5个新功能

1. Java + REPL = jshell 下一个版本的Java将使用一个名为jshell的新命令行工具,它将添加本机支持(native support)并...

3256
来自专栏企鹅号快讯

一文教会你数据库性能调优

前言 微软工程师的一个工程师曾经对性能调优有一个非常形象的比喻:剥洋葱 。我也非常认可,让我们来一层一层拨开外面它神秘的面纱。 ? 六大因素 下面祭出的是我们在...

1659

扫描关注云+社区