前言

为了找到一个适合自己的、更具操作性的、以DDD为核心的开发方法,我最近一直在摸索如何揉合BDD与DDD。围绕这个目标,我找到了Impact MappingCucumberSpock → Scala这样的一条路线,并相应选择了Scala → Spock → Cucumber这样的一条学习路线。

Spock是Java生态圈中一个新生的测试框架,采用动态语言Groovy编写。我是在阅读《BDD in Action》过程中开始接触Spock的。在该书中,作者将Spock的角色定位于取代JUnit等传统测试框架的BDD工具——在用Cucumber将Feature转译为测试步骤Step后,再通过Step粘合业务代码,同时用Spock编写相应的单元测试,以保证Feature得以正确实现。(具体内容请参见该书第49页关于验收测试与单元测试关系的描述。)

由于用Spock编写的测试也采用Gherkin语法形式的Given-When-Then三段式进行描述,可以很自然地对用例场景进行描述。《Java Testing with Spock》一书的作者Konstantinos Kapelonis提到Spock可以完全胜任BDD的需要,甚至取代Cucumber,因此我选择在学习Cucumber之前先学习Spock,并使用该书作为参考的学习资料,留下这篇书摘。

使用Cucumber实施BDD

在了解Spock之前,最好先简单熟悉一下Cucumber这个目前非常流行的BDD框架。这样无论是打算用Spock取代Cucumber,或者是将二者结合使用,都能有一个自己的判断。

注:C#配合SpecFlow实施BDD的简要过程,请参见我的另一篇文章 行为驱动开发BDD概要

第一步:定义Feature文件

Feature: 向指定账号存款

Scenario: 向A账号存入100元
Given A账号余额10元
When 向A账号存入100元
Then A账号余额为110元

第二步:将Scenario转译为若干个Step

Cucumber会将每个Scenario里的场景切分为若干个Step,然后用Java的Annotation标记

public class DepositStepDefinitions {
private Account account; @Given("^an account has (\\d+)$")
public void an_account_has_balance(int balance) {
account = new Account();
account.balance = balance;
} @When("^(\\d+) is deposited into account$")
public void amount_is_deposited(int amount) {
account.deposit(amount);
} @Then("^the balance should be (\\d+)$")
public void balance_should_be_expected(int expect) {
assertTrue(" Yes or No? ", account.balance == expect);
}
}

第三步: 采用TDD编写单元测试并实现业务代码

由于这个示例非常简单,在上面的Then Step里的断言非常简单,所以我没有再编写额外的单元测试。

public class Account {
public int balance; public Account() {
this.balance = 0;
} public void deposit(int amount) {
this.balance += amount;
}
}

使用Spock实施BDD

看过了Cucumber,再来看看Spock是怎么实现的。

@Title("向指定账号存款")
class DepositSpec extends Specification {
def "向A账号存入100元"() {
given: "A账号余额10元"
Account account = new Account()
account.balance = 10 when: "向A账号存入100元"
account.deposit(100) then: "A账号余额为110元""
account.balance == 110
}
}

Cucumber与Spock实现的简单比较

从上面的示例代码可以知,Cucumber与Spock都同样以一个Feature作为开始。但不同于Cucumber自动将Feature转译为Step,再与相应的测试代码或者业务代码粘合,Spock直接通过手工编码实现从Feature到测试代码的映射,因此相应减少了这个转译的环节。

这种区别,正是我目前非常纠结的。在《BDD in Action》一书中,作者在使用Cucumber定义了Then Step断言的情况下,又提到要使用JUnit等框架编写更细粒度的单元测试。于是,我就产生了这样的疑惑:『Step断言的粒度与单元测试断言的粒度,应该如何划分?这样的重复有必要吗?』特别是象上面的示例一样,Spock也能『自描述』Specification时。

要回答这个问题,我认为仅仅依靠《BDD in Action》中那些不完整的示例,还是比较困难的。所以我会在读完下一本书——《The Cucumber for Java Book》之后,再回头来看看是不是我对Cucumber有误解。

4-7更新

文章发表后,我和朋友们当晚就围绕我的困惑进行了讨论。终于在清晨起床的时候,突然恍然大悟。Cucumber关注的更多的是验收测试这个层面的东西,而JUnit关注的则是更细碎的单元测试层面的东西。二者关注的角度、粒度和要解决的问题都不同。所以《BDD in Action》作者提到的Cucumber+Spock的方法并没有什么不妥。当然,Spock也能胜任从单元测试、集成测试到验收测试的所有工作,所以仅仅使用Spock也未尝不可。归根结底,只是一个工具选择的问题。

使用Spock的代价

因为Cucumber有着对应不同编程语言的版本,比如Cucumber for Java, SpecFlow for .NET,因此能与团队选择的开发语言无缝衔接。反观Spock,尽管它支持Java混编,但其语法仍依赖于Groovy,因此增加了相应的学习成本,而且在工程实施过程中进行多语言的混合编程。

对此,需要提醒的是,Spock只用到了Groovy的极小部分语言特性,因此学习代价并不算很大。在业余时间照着《Java Testing with Spock》边看边做,我也只用了一周多点的时间。反倒是如何写好Feature文件,并从中筛选出Key Example,成了我最大的困扰。

至于编写好的测试,由于Cucumber与Spock都与Java兼容,因此可以直接在JUnit Runner上进行。测试报告的生成亦是如此,既可以使用JUnit辅助工具生成测试报告,也可以使用其专用的Spock ReportsDamage Control等等。

Spock入门

Groovy语法基础

Groovy和Scala一样,也是一种基于Java实现的编程语言。区别于Scala的,Groovy是动态语言。学会Spock的常见功能,只需掌握Groovy以下语法即可。

  • 语句结束用的分号;,和Scala一样是可选的。
  • Groovy将一切视作对象,==将直接调用对象的equals()方法,这与Scala同出一辙。
  • 通常用一对双引号表示一个字符串常量,"这是一个字符串常量",使用\作为转义符。
  • 用一对单引号表示不用转义其中的任何字符,类似C#里的@
  • 用成对的3个单引号或双引号表示一个保留换行格式的多行字符串,用\表示其中的换行符。这种方式通常用作大段的描述文本,比如'''这是保留了换行、缩进的RAW字符串常量'''
  • int、float等表示数字类型,使用方法类似Java,比如1.002f。
  • Groovy将0、null、空的数组或空字符串视为false,非0值、有效的引用、非空的数组和非空的字符串则均视为true
  • 使用关键字def表示动态类型,类似C#里的dynamic
  • 类的访问修饰与Scala一样,默认都是public,而field默认是private
  • 使用$作为字符串插入符,必要时使用一对大括号包围插入值,类似C# 6.0,比如return "$name, ${age > 18}"
  • 对象初始化方式类似JSON,整个表达式用圆括号包围,field与值以冒号间隔、成对出现,数组或序列用中括号包围,数组索引从0开始,用[:]表示一个空的Map
  • 闭包、lambda或者匿名函数使用大括号包围,和Scala一样,比如{count -> count * 10}
  • 使用_作为参数占位符,使用方法大致同Scala。它既可以用来指代参数、方法,也可以指代返回值或者where块中的测试参数。

Spock测试的基本结构

Spock的测试类均派生自Specification,命名遵循Java规范。每个测试方法可以直接用文本作为方法名,方法内部由given-when-then的三段式块(block)组成。除此以外,还有andwhereexpect等几种不同的块。

@Title("测试的标题")
@Narrative("""关于测试的大段文本描述""")
@Subject(Adder) //标明被测试的类是Adder
@Stepwise //当测试方法间存在依赖关系时,标明测试方法将严格按照其在源代码中声明的顺序执行
class TestCaseClass extends Specification {
@Shared //在测试方法之间共享的数据
SomeClass sharedObj def setupSpec() {
//TODO: 设置每个测试类的环境
} def setup() {
//TODO: 设置每个测试方法的环境,每个测试方法执行一次
} @Ignore("忽略这个测试方法")
@Issue(["问题#23","问题#34"])
def "测试方法1" () {
given: "给定一个前置条件"
//TODO: code here
and: "其他前置条件" expect: "随处可用的断言"
//TODO: code here when: "当发生一个特定的事件"
//TODO: code here
and: "其他的触发条件" then: "产生的后置结果"
//TODO: code here
and: "同时产生的其他结果" where: "不是必需的测试数据"
input1 | input2 || output
... | ... || ...
} @IgnoreRest //只测试这个方法,而忽略所有其他方法
@Timeout(value = 50, unit = TimeUnit.MILLISECONDS) // 设置测试方法的超时时间,默认单位为秒
def "测试方法2"() {
//TODO: code here
} def cleanup() {
//TODO: 清理每个测试方法的环境,每个测试方法执行一次
} def cleanupSepc() {
//TODO: 清理每个测试类的环境
}
}

断言

  • 在then块里,不需要assertEquals("断言提示", left, right)这样的方式,直接写left == right这样的逻辑表达式即可。
  • 借助Groovy的语法,Spock使用N * method()来判定该方法是否被调用了N次。而N * method() >> true则表示方法method被调用N次,且每次该方法的返回值均为true。
  • thrown(异常类型)断言会抛出指定类型的异常,notThrown(异常类型)则反之。

参数化测试

Spock使用where块,为测试方法提供表格化的测试数据。其中表头为测试方法中要用在断言中的变量名称或者表达式,用|分隔输入参数,用||分隔输入与输出。这些参数,可以用#参数名的方式在@Unroll描述或者测试方法名里定义,或者在测试方法的参数列表里定义,然后在where块中使用。

@Unroll("test #para0, #para1")  //where块中的每行参数都转换为一个独立的测试用例
def "测试方法3 #para2, #para3"(int first, int second) {
... ...
where: "parameterized sample"
para0 | para1 | para2 || para3 | first | second
10 | 2 | 3 || 7 | 2 | 5
}

除以上这种表格式的数据,Groovy支持<<<<<操作符作为输入源,用>>操作符指示输出,还有类似Scala的1 .. 10这样生成序列的语法。

array << [0, 5, 7, 9]
list << (1 .. 10)
[name, condition] << [["Jack", true], ["Tom", false]]
isConfirmed() >> true
getAnswers() >>> [1, 2, 5, 7] //顺次返回列表里的值
isAvailable(_) >>> true >> false //首次返回true,第2次返回false

Stub与Mock

Spock提供Stub与Mock功能,主要应用于单元测试。其使用很简单,直接Stub(类或接口)Mock(类或接口)即可。

given: "preparation"
Inventory inventory = Stub(Inventory) {
location >> "LOS"
manager >> "Tom"
} DeliveryProcessor processor = Mock(DeliveryProcessor) when: "one order is confirmed"
order.Confirm() then: "only serviceA is called"
1 * processor.serviceA(_)
0 * processor._

原则上,任何的Stub都可以用Mock代替,反之而不尽然。因为Stub是辅助测试的工具,它模拟或者说伪装成一个真正的对象,为测试提供一个完整的环境(输入);而Mock才是验证对象行为的工具,虽然它也是对象的一个假体,但其目的是确定对象是否按正常预期工作(输出)。

Maven配置

加入Groovy支持

<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.6</version>
<configuration>
<useFile>false</useFile>
<includes>
<include>**/*Spec.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>

加入Spock支持

<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.0-groovy-2.4</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes
(in addition to interfaces) -->
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>3.1</version>
<scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes without default
constructor (together with CGLIB) -->
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>

结语

在该书的最后一部分,作者就Spock在企业应用开发领域进行规模化应用进行了较为详尽的阐述,包括如何组织Spock测试,如何与Maven集成,如何协调单元测试、集成测试和功能测试,如何使用Geb和REST工具辅助测试等等。在附录中,也详细介绍了Spock如何与Eclipse等常见IDE进行集成。有兴趣的,可以读一读这本书。

基于Groovy动态语言的强大功能,Spock框架仍在不断发展,因此这篇书摘意义的文章只算是管中窥豹。特别是在具体实践BDD的过程中,究竟是仅靠Spock,亦或Cucumber+Spock的方式,都是需要通过学习和实践来确认的。

BDD测试框架Spock概要的更多相关文章

  1. 在Spring Boot项目中使用Spock测试框架

    本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...

  2. Selenium 4 Java的最佳测试框架

    几十年来,Java一直是开发应用程序服务器端的首选编程语言.尽管JUnit一直在与开发人员一起帮助他们进行自动化的单元测试,但随着时间的推移和测试行业的发展,特别是伴随着自动化测试的兴起,已经开发了许 ...

  3. iOS开发中的测试框架

    转载作者:@crespoxiao 我们为什么要用测试框架呢?当然对项目开发有帮助了,但是业内现状是经常赶进度,所以TDD还是算了吧,BDD就测测数据存取和重要环节,这很重要,一次性跑完测试单元检查接口 ...

  4. iOS开发中的测试框架 (转载)

      作者:CrespoXiao授权 地址:http://www.jianshu.com/p/7e3f197504c1 我们为什么要用测试框架呢?当然对项目开发有帮助了,但是业内现状是经常赶进度,所以T ...

  5. Python BDD自动化测试框架初探

    1. 什么是BDD BDD全称Behavior Driven Development,译作"行为驱动开发",是基于TDD (Test Driven Development 测试驱动 ...

  6. 收藏清单: python测试框架最全资源汇总

    xUnit frameworks 单元测试框架 frameworks 框架 unittest - python自带的单元测试库,开箱即用 unittest2 - 加强版的单元测试框架,适用于Pytho ...

  7. PHP自动测试框架Top 10

    对于很多PHP开发新手来说,测试自己编写的代码是一个非常棘手的问题.如果出现问题,他们将不知道下一步该怎么做.花费很长的时间调试PHP代码是一个非常不明智的选择,最好的方法就是在编写应用程序代码之前就 ...

  8. python测试框架&&数据生成&&工具最全资源汇总

    xUnit frameworks 单元测试框架frameworks 框架unittest - python自带的单元测试库,开箱即用unittest2 - 加强版的单元测试框架,适用于Python 2 ...

  9. behave 测试框架,了解一下

    # behave测试框架 [behave](https://pythonhosted.org/behave/)是python的1个bdd测试框架实现. ### 安装 ```pip install be ...

随机推荐

  1. BZOJ3236: [AHOI2013]作业

    BZOJ3236: [AHOI2013]作业 题目描述 传送门 行,我知道是Please contact lydsy2012@163.com! 传送门2 题目分析 这题两问还是非常,emmmm. 首先 ...

  2. 从0开始 数据结构 AC自动机 hdu 2222

    参考博客 失配指针原理 使当前字符失配时跳转到另一段从root开始每一个字符都与当前已匹配字符段某一个后缀完全相同且长度最大的位置继续匹配,如同KMP算法一样,AC自动机在匹配时如果当前字符串匹配失败 ...

  3. Ubuntu+apache安装redmin

    公司要迁移redmin,本来以为是一个很简单的项目,想不到整整搞了一天加一个晚上. 首先是对ruby的安装不熟悉,现在明白了ruby的安装顺序是先安装rvm版本管理,然后用rvm安装ruby,安装好后 ...

  4. vue.js学习之组件(上篇)

    本文的Demo和源代码已放到GitHub,如果您觉得本篇内容不错,请点个赞,或在GitHub上加个星星! https://github.com/zwl-jasmine95/Vue_test 以下所有知 ...

  5. 关于Gulp

    Gulp & webpack 配置详解http://www.jianshu.com/p/2d9ed1fe3e8c 使用 Gulphttp://hwaphon.site/?p=439 前端构建工 ...

  6. Tomcat服务部署步骤

    Tomcat服务部署步骤 1. 2. 3. tar -zxvf apache-tomcat-7.0.68.tar.gz,然后修改文件夹名称为需要的名称, 使用mv命令 4. 删除 /webapps/R ...

  7. C#区块链零基础入门,学习路线图 转

    C#区块链零基础入门,学习路线图 一.1分钟短视频<区块链100问>了解区块链基本概念 http://tech.sina.com.cn/zt_d/blockchain_100/ 二.C#区 ...

  8. HDU-4714-贪心

    Tree2cycle Time Limit: 15000/8000 MS (Java/Others)    Memory Limit: 102400/102400 K (Java/Others)Tot ...

  9. su | sudo su | sudo -i

    su <user> <user> <user> 需要输入user的密码,该命令改变user id,执行过后,以<user>中定义的用户运行shell,就 ...

  10. IOS-简单动画

    iOS那些简单的动画(不定期更新) 字数669 阅读1118 评论16 喜欢59 关于 Core Animation Core Animation是一组非常强大的动画处理API,使用它能做出很多优雅的 ...