实习笔记(三)groovy/scope

Groovy/Spock 测试导论

测试对于软件开发者而言至关重要,不过总会有人说:“写代码是我的事,测试那是QA的工作”,这样的想法真是弱爆了,因为大量的业界实践已经证明测试驱动编码可以有效地帮助开发者提升代码质量。

   
   

Spock 集成了Junit, JMock 和RSpec 等测试框架的优势, 使开发者能够使用BDD DSL 语言进行测试代码的编写.

它完全兼容Junit, 同时不需要依赖任何的Mock 框架(如Mockito).

关于Spock 技术的更多信息, 请参考Spock Primer.

在Spock中,待测系统(system under test; SUT) 的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类需要继承自Specification类。

在这里, 给出Spock 与JUnit 的术语对比表. 以增加大家的直观理解.

SpockJUnit
SpecificationTest class
setup()@Before
cleanup()@After
setupSpec()@BeforeClass
cleanupSpec()@AfterClass
FeatureTest
Feature methodTest method
Data-driven featureTheory
ConditionAssertion
Exception condition@Test(expected=…)
InteractionMock expectation (e.g. in Mockito)

4.3.5.blocks

每个feature method又被划分为不同的block,不同的block处于测试执行的不同阶段,在测试运行时,各个block按照不同的顺序和规则被执行,如下图:

使用Spock框架进行单元测试

下面分别解释一下各个block的用途。

4.3.6.Setup Blocks

setup也可以写成given,在这个block中会放置与这个测试函数相关的初始化程序,如:


setup:
def stack = new Stack()
def elem = "push me"
1
2
3
setup  :
def  stack  =  new  Stack  (  )
def  elem  =  "push me"


一般会在这个block中定义局部变量,定义mock函数等。

4.3.7.When and Then Blocks

when与then需要搭配使用,在when中执行待测试的函数,在then中判断是否符合预期,如:


when:
stack.push(elem)  

then:
!stack.empty
stack.size() == 1
stack.peek() == elem
1
2
3
4
5
6
7
when  :
stack  .  push  (  elem  )    
 
then  :
!  stack  .  empty
stack  .  size  (  )  ==  1
stack  .  peek  (  )  ==  elem


4.3.7.1.断言

条件类似junit中的assert,就像上面的例子,在then或expect中会默认assert所有返回值是boolean型的顶级语句。如果要在其它地方增加断言,需要显式增加assert关键字,如:


def setup() {
  stack = new Stack()
  assert stack.empty
}
1
2
3
4
def  setup  (  )  {
    stack  =  new  Stack  (  )
    assert  stack  .  empty
}


4.3.7.2.异常断言

如果要验证有没有抛出异常,可以用thrown(),如下:


when:
stack.pop()  

then:
thrown(EmptyStackException)
stack.empty
1
2
3
4
5
6
when  :
stack  .  pop  (  )    
 
then  :
thrown  (  EmptyStackException  )
stack  .  empty


要获取抛出的异常对象,可以用以下语法:


when:
stack.pop()  

then:
def e = thrown(EmptyStackException)
e.cause == null
1
2
3
4
5
6
when  :
stack  .  pop  (  )    
 
then  :
def  e  =  thrown  (  EmptyStackException  )
e  .  cause  ==  null


如果要验证没有抛出某种异常,可以用notThrown():


def "HashMap accepts null key"() {
  setup:
  def map = new HashMap()  

  when:
  map.put(null, "elem")  

  then:
  notThrown(NullPointerException)
}
1
2
3
4
5
6
7
8
9
10
def  "HashMap accepts null key"  (  )  {
    setup  :
    def  map  =  new  HashMap  (  )    
 
    when  :
    map  .  put  (  null  ,  "elem"  )    
 
    then  :
    notThrown  (  NullPointerException  )
}


4.3.8.Expect Blocks

expect可以看做精简版的when+then,如:


when:
def x = Math.max(1, 2)  

then:
x == 2
1
2
3
4
5
when  :
def  x  =  Math  .  max  (  1  ,  2  )    
 
then  :
x  ==  2


可以简化为:


expect:
Math.max(1, 2) == 2
1
2
expect  :
Math  .  max  (  1  ,  2  )  ==  2


4.3.9.Cleanup Blocks

函数退出前做一些清理工作,如关闭资源等。

4.3.10.Where Blocks

做测试时最复杂的事情之一就是准备测试数据,尤其是要测试边界条件、测试异常分支等,这些都需要在测试之前规划好数据。但是传统的测试框架很难轻松的制造数据,要么依赖反复调用,要么用xml或者data provider函数之类难以理解和阅读的方式。比如说:


class MathSpec extends Specification {
    def "maximum of two numbers"() {
        expect:
        // exercise math method for a few different inputs
        Math.max(1, 3) == 3
        Math.max(7, 4) == 7
        Math.max(0, 0) == 0
    }
}
1
2
3
4
5
6
7
8
9
class  MathSpec  extends  Specification  {
      def  "maximum of two numbers"  (  )  {
          expect  :
          // exercise math method for a few different inputs
          Math  .  max  (  1  ,  3  )  ==  3
          Math  .  max  (  7  ,  4  )  ==  7
          Math  .  max  (  0  ,  0  )  ==  0
      }
}


而在spock中,通过where block可以让这类需求实现起来变得非常优雅:


class DataDriven extends Specification {
    def "maximum of two numbers"() {
        expect:
        Math.max(a, b) == c

        where:
        a | b || c
        3 | 5 || 5
        7 | 0 || 7
        0 | 0 || 0
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
class  DataDriven  extends  Specification  {
      def  "maximum of two numbers"  (  )  {
          expect  :
          Math  .  max  (  a  ,  b  )  ==  c
 
          where  :
          a  |  b  ||  c
          3  |  5  ||  5
          7  |  0  ||  7
          0  |  0  ||  0
      }
}


上述例子实际会跑三次测试,相当于在for循环中执行三次测试,a/b/c的值分别为3/5/5,7/0/7和0/0/0。如果在方法前声明@Unroll,则会当成三个方法运行。

更进一步,可以为标记@Unroll的方法声明动态的spec名:


class DataDriven extends Specification {
    @Unroll
    def "maximum of #a and #b should be #c"() {
        expect:
        Math.max(a, b) == c

        where:
        a | b || c
        3 | 5 || 5
        7 | 0 || 7
        0 | 0 || 0
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class  DataDriven  extends  Specification  {
      @  Unroll
      def  "maximum of #a and #b should be #c"  (  )  {
          expect  :
          Math  .  max  (  a  ,  b  )  ==  c
 
          where  :
          a  |  b  ||  c
          3  |  5  ||  5
          7  |  0  ||  7
          0  |  0  ||  0
      }
}


运行时,名称会被替换为实际的参数值。

除此之外,where block还有两种数据定义的方法,并且可以结合使用,如:


where:
a | _
3 | _
7 | _
0 | _

b << [5, 0, 0]

c = a > b ? a : b
1
2
3
4
5
6
7
8
9
where  :
a  |  _
3  |  _
7  |  _
0  |  _
 
b  <<  [  5  ,  0  ,  0  ]
 
c  =  a  >  b  ?  a  :  b

大多数遵循TDD的Java开发者均会使用mockito或powermock,但mockito和powermock均包含了许多样本代码,导致测试代码变得冗长而难以维护。在测试中引入Groovy/Spock后,我完全被它们吸引,并转向使用Groovy/Spock来替代原有的测试框架。

下面将围绕一个简单例子来讲解Groovy/Spock,例子中将包含一个service类,负责处理domain对象,以及一个数据访问层。
首先是domain类:

public class User {

    private int id; private String name; private int age; // Accessors omitted }

接下来是DAO接口:

public interface UserDao {

    public User get(int id); }

最后是service类:

public class UserService {

    private UserDao userDao;
    
    public UserService(UserDao userDao) { this.userDao = userDao; } public User findUser(int id){ return null; } }

采用Groovy/Spock针对UserService编写测试

class UserServiceTest extends Specification { UserService service UserDao dao = Mock(UserDao) def setup(){ service = new UserService(dao) } def "it gets a user by id"(){ given: def id = 1 when: def result = service.findUser(id) then: 1 * dao.get(id) >> new User(id:id, name:"James", age:27) result.id == 1 result.name == "James" result.age == 27 } }

上述测试代码中,首先我们使用了groovy,这是一种非常类似Java的语言,但是它的语法更加轻,例如它不用像Java语言那样,在每句结尾加上分号;它也不需要使用public修饰符,因为public是默认的。上述测试类继承自spock.lang.Specification,这是Spock基类,继承该基类后就可以使用given,when,then等代码块

在Spock中创建mock对象非常容易,只需要使用Mock(Class)这样的语句即可。如上所述,mock后的DAO对象被传入userService中。Setup方法会在每个测试方法运行前被执行

Groovy的一个显著特点是可以使用字符串文本来命名方法,将这个特点应用在测试方法上就能使得测试方法可以更加容易被阅读和理解,如上述代码所示。

Given, when, then

Spock是一个BDD测试框架,因此对于Spock中涉及的given,when,then样式最简单的理解就是:
Given 给定一些条件,When 当执行一些操作时,Then 期望得到某个结果。

如上述测试方法中Given,给定id=1,即测试的变量;而在When中则是被测试方法,如在上述代码中调用findUser();Then中则是断言,即检查被测试方法的输出结果。

上述Then中的第一句语句虽然看上去可怕,但实际上却非常容易理解:

1 * dao.get(id) >> new User(id:id, name:"James", age:27)

该行表示了对于mock对象dao的期望值,即期望调用dao.get()方法1次,而“>>”是spock的特色,表示“then return”含义。因此该句翻译过来的意思是:期望调用1次dao.get()方法,当执行该方法后,请返回一个新的User对象。此外在构造方法中使用具名参数也是groovy的另一特点。Then中剩余的代码对result对象进行检查。

由此测试代码驱动产生的产品代码非常简单,如下所示:

public class UserService {

    private UserDao userDao;
    
    public UserService(UserDao userDao) { this.userDao = userDao; } public User findUser(int id){ return userDao.get(id); } }

接下来实现创建用户功能,在UserService中添加如下代码:

public void createUser(User user){ // check name // if exists, throw exception // if !exists, create user }

在UserDao中添加如下方法:

public User findByName(String name);
public void createUser(User user);

相应的测试方法如下:

def "it saves a new user"(){ given: def user = new User(id: 1, name: 'James', age:27) when: service.createUser(user) then: 1 * dao.findByName(user.name) >> null then: 1 * dao.createUser(user) }

在上述代码中出现了两处Then,这是因为当所有断言放在一个then块中,Spock会认为这些断言是同时发生的。如果期望断言按顺序执行,则需要将断言分割到多个then块中,spock会按顺序执行断言。如上述所示,首先需要判断用户是否存在,然后再去创建用户。产品代码实现如下:

public void createUser(User user){ User existing = userDao.findByName(user.getName()); if(existing == null){ userDao.createUser(user); } }

上述代码针对用户不存在场景,而对于用户存在的场景,测试代码如下:

def "it fails to create a user because one already exists with that name"(){ given: def user = new User(id: 1, name: 'James', age:27) when: service.createUser(user) then: 1 * dao.findByName(user.name) >> user then: 0 * dao.createUser(user) then: def exception = thrown(RuntimeException) exception.message == "User with name ${user.name} already exists!" }

上述代码当调用findByName时,返回一个存在的用户,然后不调用createUser(),第三个Then块捕获方法抛出的异常。注意groovy拥有一个称之为GStrings的特征,该特征可以在引用的字符串中插入参数,如${user.name}。相应产品代码如下:

public void createUser(User user){ User existing = userDao.findByName(user.getName()); if(existing == null){ userDao.createUser(user); } else{ throw new RuntimeException(String.format("User with name %s already exists!", user.getName())); } }

提示

  • 最重要也是最容易被遗忘的提示,阅读spock文档!
  • 可以命名spock块,例如将given命名为“Some variables”,有助于开发者在测试代码中更加清楚的表达含义
  • 当对mock对象方法调用次数不关心时,可以使用_ * mock.method()
  • 在then块中可使用下划线来通配方法及类,例如,0 * mock._ 表示期望mock对象的任何方法都未被调用,或0 * . 表示期望任何对象的任何方法都未被调用
  • 通常按given,when,then编写测试,但实际上从when开始编写测试会更加容易发现测试需要的given和测试的输出结果(then)
  • expect块对于测试不需要对mock对象进行断言的简单方法更加有效
  • 当对于传递给mock对象的参数不关注时,可以使用通配符参数
  • 拥抱groovy闭包Embrace groovy closures! They can be you’re best friend in assertions!
  • 当希望在整个测试类中只运行一次,可以复写setupSpec和cleanupSpec

结论

测试代码是为了协助开发者的,而不是起相反作用,groovy在这方面提供了很多快捷方式来帮助开发者写出更加优雅的测试代码。完整代码可参考https://gist.github.com/jameselsey/8096211

思考

翻译这篇文章是受到了《使用 Groovy 语言替代 JUnit 来为 Java 程序编写单元测试》和《The Coding Kata: FizzBuzzWhizz in Modern Java》两篇文章的启示。除了赞叹两篇文章中采用的测试框架的易用,也深深地被groovy所吸引,其作为DSL的特质不论是对于追求编写更好测试用例的精益开发者还是对于刚入门测试用例的新手开发者来说都是容易掌握和使用的。我们期望测试用例的目标就是能够作为产品代码的 living docs,最佳的效果就是完全摆脱编程语言的语法束缚,成为纯粹的书写或口头表达方式,这样就能“望文生义”。Groovy在这方面确实对于Java测试用例编写起到了促进作用,再加上groovy与Java的无缝融合,及自身拥有的语法特性,在团队中推广groovy替代传统Java测试框架的唯一阻力就剩下大多数开发者是否愿意学习一门新的编程语言。

以一种直观的方式编写参数化测试

Spock可以提供数据表格来使得单元测试更容易理解。由于其使用了Groovy DSL(Domain specific language)领域专用语言,因此使得你可以讲数据和其描述以表格的方式放在一起:

public void "Valid images are PNG and JPEG files"() {
        given: "an image extension checker"
        ImageNameValidator validator = new ImageNameValidator()

        expect: "that only valid filenames are accepted"
        validator.isValidImageExtension(pictureFile) == validPicture

        where: "sample image names are"
        pictureFile        || validPicture
        "scenery.jpg"      || true
        "house.jpeg"       || true
        "car.png"          || true
        "sky.tiff"         || false
        "dance_bunny.gif"  || false
}
第 19 段(可获 0.89 积分)

增加一个新的输入或者输出变量也是相当的容易,因为你只需要给表格增加一列即可。

Spock data table

有趣的是,如果你运行单独的这一个Spock测试,而且使用了Spock 的 Unroll 注解的话,

@Unroll("Running image #pictureFile with result #validPicture")
public void "Valid images are PNG and JPEG files"() {
        given: "an image extension checker"
        ImageNameValidator validator = new ImageNameValidator()

        expect: "that only valid filenames are accepted"
        validator.isValidImageExtension(pictureFile) == validPicture

        where: "sample image names are"
        pictureFile        || validPicture
        "scenery.jpg"      || true
        "house.jpeg"       || true
        "car.png"          || true
        "sky.tiff"         || false
        "dance_bunny.gif"  || false
}

这里是运行结果:

Spock Unroll annotation

这对于拥有大量数目的测试来说尤其有用。如果其中一个测试失败了,你就可以在测试报告中看出哪个测试失败了(而不是将整个测试都标记为失败)。

Spock数据表格是参数化测试最基本的形式。Spock同样支持数据管道和自定义迭代器,可以满足对输入输出参数处理的更强大的需求。

在以后的文章里我们将会探索Spock所提供的关于参数化测试的所有功能 - 因为它们值得我们对其单独进行分析。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值