爱上单元测试:基于Groovy的Spock框架

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

Spock测试框架基于Groovy并吸收了Junit、TestNG、Mockito等测试框架的优点。
Spock编写的单元测试层次清晰,代码量少,可读性好。
Groovy无缝兼容Java:Groovy最终会编译为class文件,JVM并不在乎class来自Java还是Groovy文件,支持各种集成开发环境(eclipse,Intellij Ieda),尤其是Intellij idea已经集成支持Groovy的插件,也支持maven-surefire-plugin、jacoco等maven插件。

这里写图片描述

1.学习前的准备

官网:http://spockframework.org
必读书籍:《Java Testing with Spock》
如要速成只需要阅读以下两篇文章:
5分钟入门Groovy: https://learnxinyminutes.com/docs/groovy/
一篇非常详尽的介绍Spock的英文教程:https://semaphoreci.com/community/tutorials/stubbing-and-mocking-in-java-with-the-spock-testing-framework

2.Maven依赖

Eclipse支持Groovy需要安装插件:https://github.com/groovy/groovy-eclipse/wiki
注意Maven作用域控制在test

<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-core</artifactId>
  <version>1.1-groovy-2.4</version>
  <scope>test</scope>
</dependency>
<dependency> <!-- enables mocking of classes (in addition to interfaces) -->
  <groupId>net.bytebuddy</groupId>
  <artifactId>byte-buddy</artifactId>
  <version>1.6.5</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.5.1</version>
  <scope>test</scope>
</dependency>

3. 被测试的类

  //数据库映射类
  public class DbEntity {
    long id;
    String requestNo;
    //省略get set
}
//被测试的service
@Component
public class MyService {
    @Autowired
    private DbEntityDao dbEntityDao;
    private String value;

    public String getValue() {
        return value;
    }

    public int stringToInteger(String valueStr) {
        return Integer.valueOf(valueStr);
    }

    public String getUtilValue(String id) {
        return id+MyServiceUtil.getValue(id);
    }

    public void setValue(String value) {
        this.value = value;
    }
    public long selectByRequestNo(String requestNo){
        DbEntity dbEntity =  dbEntityDao.selectByRequestNo(requestNo);
        return dbEntity.getId();
    }

    public int add(int a,int b){
        return a+b;
    }

}
//数据库操作类
public interface DbEntityDao {

    DbEntity selectByRequestNo(@Param("requestNo")String requestNo);
}
//工具类
public class MyServiceUtil {
    public static String getValue(String value){
        return "MyServiceUtil";
    }
}

4. given-when-then声明

given-when-then是Spock的基本句式,使单元测试层次清晰。
此外还有and关键字,用于将大段的声明分割开来。
所有的Spock单元测试都继承spock.lang.Specification,Specification基于Groovy dsl提供了测试环境。
Spock支持将一个句子作为方法名,实现自解释。
Stub用来创建要模拟的测对象。
“>>”用来表示模拟对象的返回值
“>>>”用来表示同一方法多次按顺序调用返回不同值
下划线“_”表示匹配所有的输入值。
Spock不使用Assert来校验结果,then声明后面的表达式result == “1”就相当于Junit中的Assert.assertTrue(result.equals(“1”))
下面通过例子展示以下用法,Groovy可以不写分号

 import spock.lang.Specification

/**
 * JUnit 的测试用例总是由 Runner 去执行
 * Specification的注解:@RunWith(Sputnik.class) 是对 org.junit.runner.Runner的扩展
 */
class MyServiceTest extends Specification {
    //groovy里面用def定义所有的对象,包含方法声明
    def mockRequestNo = "123"
    def "最基本的测试:mock返回值"() {
        given: "given用来准备mock对象,可以放到when里面"
        and:"and声明可选"
        myService.getValue() >> "1"
        when: "when里面调用被测试的方法"
        String result = myService.getValue()
        then: "then用来验证结果"
        result == "1"
    }

  def "最基本的测试:多次调用返回不同值"() {
        given: "返回多个值使用三个>"
        MyService myService = Stub(MyService)
        myService.getValue() >>> ["1", "2", "3"]
        //except相当于when then的合并
        expect:
        myService.getValue() == "1"
        myService.getValue() == "2"
        myService.getValue() == "3"
    }
   def "测试模糊匹配"() {
        given:
        MyService myService = Stub(MyService)
        //下划线表示匹配所有输入值
        myService.stringToInteger(_) >> 999
        //except相当于when then的合并
        expect: "有时候我们并不在乎输入值"
        myService.stringToInteger("1") == 999
        myService.stringToInteger("2") == 999
        myService.stringToInteger("3") == 999
    }
  }

5. 校验模拟对象的行为

Stub只能模拟对象的返回值,而Mock更进一步,不仅能模拟对象,还能校验对象的调用次数等行为。
表达式: N * mockedObject.method(arguments)>>value,表示参数为arguments的method方法调用N次,返回值是value。该表达式一般放在then后面。
注意,尽管then声明放在when后面,由于基于Groovy AST语法解析树,Spock会先解析该表达式,然后在when后面的测试类执行后再进入then后面的校验逻辑。
Groovy里面一切field都为public,免去了注入的烦恼
Groovy的lambda表达式比Java 8更加灵活,通过lambda可以抓取测试对象的输入值,嵌套很深的测试类很有必要验证一下输入值是否正确。

   def mockRequestNo = "123"
   def "校验mock对象的调用次数"() {
        given:
        DbEntity expectEntity = new DbEntity()
        //groovy的with语法,简化创建对象
        expectEntity.with {
            id = 456
            requestNo = mockRequestNo
        }
        //mock跟Stub不同的是可以校验mock对象的调用次数
        DbEntityDao dao = Mock(DbEntityDao)
        MyService myService = new MyService()
        //Groovy里面所有的field都是public的,可以直接访问
        myService.dbEntityDao = dao
        when:
        long id = myService.selectByRequestNo(mockRequestNo)
        then:
        id == 456
        //then除了验证结果,还可以设置mock对象返回值,
        //Spock会先解析then中定义的mock,等when执行后再校验mock行为
        1 * dao.selectByRequestNo(mockRequestNo) >> expectEntity
    }

    def "抓取输入值"(){
        given:
        def resultCapture = null
        DbEntityDao dao = Stub(DbEntityDao.class)
        //定义lambda表达式,抓取输入值
        dao.selectByRequestNo({v-> resultCapture = v })>>null
        when:
        dao.selectByRequestNo("123")
        then:
        resultCapture == "123"
    }

6. 参数化测试

Spock的参数化测试比Junit更加简洁。
直接使用表格形式来定义输入值跟期望值。
注意输入的参数名必须跟被测试方法参数名一致。
如下代码,表格中的输入参数跟测试方法输入参数名均为a,b

  def "test Parameterized"() {
        when:
        MyService myService = new MyService()
        then:
        myService.add(a, b) == result
        where: "准备参数,输入参数名必须跟方法里面的参数名一致"
        a | b || result
        1 | 1 || 2
        1 | 2 || 3
        2 | 2 || 4
    }

7. 同其他测试框架混搭

对于static、private方法,Spock还是无能为力。这时候可以结合PowerMock框架。
JUnit 的测试用例总是由 Runner 去执行,JUnit 提供了 @RunWith 注解来指定自定义的 Runner。
如果未指定特别的 Runner,那么会采用默认的 Runner
由于Spock的Runner直接继承自Junit Runner可以很好的扩展。下面展示如何跟PowerMock集成。

import spock.lang.Specification

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([MyServiceUtil.class])
class MyServiceWithOtherRunner extends Specification{

    def id = "1024"
    def "测试难缠的static方法"(){
        given:
        PowerMockito.mockStatic(MyServiceUtil.class)
        PowerMockito.when(MyServiceUtil.getValue(id)).thenReturn("horrible")
        MyService myService = new MyService();
        when:
        String result = myService.getUtilValue(id)
        then:
        result == id+"horrible"
    }
}

8.其他


该小节展示一些杂项

类似于Junit注解的声明

def setupSpec() { //整个单元测试启动的时候只执行一次,类似Junit的@BeforeClass
    println "Will run only once"
} 
//每个方法执行前都会执行,等同Junit @Before
def setup() {
  println "Will run before EACH feature"
} 
def cleanup() { //执行后每次执行
println "Will run once after EACH feature"
}
def cleanupSpec() { //整个单元测试结束后执行
println "Will run once at the end"
} 

模拟异常

  def "test trade"() {
        given:
          mockObject.method() >> { throw new RuntimeException("哈哈,中计了") }
        when: "踩坑"
          mockObject.method()
        then: "validator"
         RuntimeException runtimeException = thrown(RuntimeException)
         runtimeException.message == "哈哈,中计了"

    }

简化校验

then:
 mockObject.value1 == 1
 mockObject.value2 == 2
 mockObject.value3 == 3
 mockObject.value4 == 4

得益于Groovy with闭包,还可以简化成这样
then:
 with(mockObject){
   value1 == 1
   value2 == 2
   value3 == 3
   value4 == 4
 }

 同样使用Stub初始化对象也可以写成这样,isEmpty()等均是类WarehouseInventory里面的方法。
when:
 WarehouseInventory inventory = Stub(WarehouseInventory) {
  isProductAvailable("bravia",1) >> true
  isProductAvailable("panasonic",1) >> false
  isEmpty() >> false
 }

猜你喜欢

转载自blog.csdn.net/j16421881/article/details/80358325