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

相关文章

来自专栏PPV课数据科学社区

大规模爬虫流程总结

爬虫是一个比较容易上手的技术,也许花5分钟看一篇文档就能爬取单个网页上的数据。但对于大规模爬虫,完全就是另一回事,并不是1*n这么简单,还会衍生出许多别的问题。...

27611
来自专栏程序员的SOD蜜

领域驱动设计(DDD)技术分享

注:本文为技术讨论会上的内容要点摘录整理的,相关内容仅作参考。 1       “模型”的几个概念 下面这2个名词容易混淆: Module---模块,通常按照功...

3219
来自专栏Java架构

Java就业指导

1572
来自专栏SDNLAB

SDN实战团分享(六):OpenDayLight实战入门

今天讲的是SDN的实战,但说到实战,可能还是牵强了点,SDN技术代表了某种意义上的未来,所以现在来说,并不是大规模应用的阶段。所以,我们可以去设想SDN会在时候...

2728
来自专栏美团技术团队

“小众”之美——Ruby在QA自动化中的应用

1743
来自专栏即时通讯技术

新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

Netty 是一个广受欢迎的异步事件驱动的Java开源网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

843
来自专栏SDNLAB

SDN实战团分享(六):OpenDayLight实战入门

今天讲的是SDN的实战,但说到实战,可能还是牵强了点,SDN技术代表了某种意义上的未来,所以现在来说,并不是大规模应用的阶段。所以,我们可以去设想SDN会在时候...

2597
来自专栏张善友的专栏

ADO.NET 实体框架概述

随着.NET Framework 3.5 SP1和Visual Studio 2008 SP1的正式发布。ADO.NET 实体框架正式来到开发人员的面前,它使开...

1915
来自专栏跨界架构师

如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文

  前几篇已经实现了一个最简单的购买过程,这次开始往这个过程中增加一些东西。比如促销、会员价等,在我们的第一篇文章(如何一步一步用DDD设计一个电商网站(一)—...

892
来自专栏Petrichor的专栏

硬件: 高性价比的 工作站配置 列表

[1] CPU性能天梯图 [2] GPU性能天梯图 [3] GeForce 10 series [4] 为个人深度学习机器选择合适的配置 [5]...

863

扫码关注云+社区