单元测试框架Spock【开发实践】

单元测试框架Spock【开发实践】Spock 单元测试框架 spock

大家好,欢迎来到IT知识分享网。

一、基础知识与背景

1.1 单元测试和集成测试

单元测试:指对软件中的最小可测试单元进行的检查和验证。
集成测试:在所有组件或模块集成在一起之后进行的测试,目的是检查这些组件或模块作为一个整体是否能够协同工作。
例:直接测试Controller方法,就是集成测试。测试Controller方法内部调用的子方法,就是单元测试。

1.2 单元测试的目的

尽早在尽量小的范围内暴露错误,降低修复成本。
提高测试的代码覆盖率,降低上线时的紧张指数。
减少代码中的bug数量,bug数量和kpi挂钩,直接关系到升职加薪。

1.3 主流的单元测试工具

工具 原理 最小Mock单元 对被Mock方法的限制 上手难度 IDE支持
Mockito 动态代理 不能Mock私有/静态和构造方法 较容易 很好
Spock 动态代理 不能Mock私有/静态和构造方法 较复杂 一般
PowerMock 自定义类加载器 支持任何方法 较复杂 较好
JMockit 运行时字节码修改 不能Mock构造方法 较复杂 一般
TestableMock 运行时字节码修改 方法 支持任何方法 很容易 一般

1.4 为什么要用Spock

Spock测试框架基于Groovy,并吸收了许多其他测试框架的优点,其主要特点如下:

  • 使用Groovy这种动态语言来编写测试代码,无缝兼容Java(Groovy最终会编译为class文件),Intellij idea已经集成支持Groovy的插件。
  • 遵从BDD(行为驱动开发)模式,让测试代码更规范,有助于提升代码的质量,测试代码更具可读性,降低后期维护难度。
  • 支持Mock对象和Stubbing行为。
  • 通过where块来实现数据驱动测试,大大减小测试代码的数量,还有助于提升测试的代码覆盖率。

1.5 一些术语

  • BBD模式:强调以自然语言描述测试行为,使得测试代码更易于理解且与业务需求紧密关联。测试用例通常由given-when-then三段式结构组成,分别代表设置测试环境和初始数据、触发被测试方法或操作、断言预期结果或系统状态。
  • Mock对象:Mock对象是对真实对象的一种模拟,它具有与真实对象相似的接口,但在测试期间并不执行实际的操作。而是根据测试的需要,预先设定好对方法调用的响应。使用Mock对象来模拟或替换系统中某些不容易构造或获取的对象,通过创建其替代品(即 Mock 对象)来模拟其预期行为,使测试能够专注于被测代码的逻辑,而不受外部因素干扰。
  • 数据驱动测试:数据驱动测试(Data-Driven Testing, DDT)是一种软件测试方法,其核心理念是将测试逻辑与测试数据分离,使得测试用例可以根据不同的数据集重复执行相同的测试流程。这种方法旨在提高测试覆盖率、减少重复工作,并增强测试的灵活性与可维护性(对应where块)。
  • 代码覆盖率:衡量的是在执行测试集时,程序中哪些代码行、分支、条件、函数等编程元素被执行过,以及它们被执行的比例。
  • 断言:断言用于在代码位置上强制检查某个条件是否为真。如果条件为真(即预期的情况发生),程序继续正常执行。如果条件为假(即出现了未预期的状态),断言会触发一个错误,导致程序停止执行。

二、使用Spock框架进行单元测试

2.1 SpringBoot集成Spock框架Demo

@SpringBootTest class UserServiceSpec extends Specification { 
    @Autowired private UserService userService // 测试方法 def "test description"() { 
    // given块 given: "initial conditions" // 设置前置条件或数据 // when块 when: "action is performed" // 执行被测试方法或操作 // then块 then: "expected outcome" // 验证结果或状态 // where块 where: value | expected "input1" | "output1" "input2" | "output2" } } 
  • Spock测试类的工程目录:src/test/groovy
  • Spock测试类的文件格式:XxxSpec.groovy。Spock测试类的类名建议也使用大驼峰命名格式,且使用Spec作为后缀名。Spock测试类的扩展名为groovy。
  • Spock测试类的格式:如上图,Spock测试类看起来像没有权限修饰符的java类,且必须继承自Specification。Spock测试类的内部和java不同,使用了Groovy语法,由一个或多个测试方法组成。
  • Spock测试类上加@SpringBootTest的作用:用于启动Spring应用上下文,在单元测试时不需要,在集成测试时测试类可以利用@Autowired注入所需的Bean。

2.2 测试方法的基本结构

测试方法的方法体由given-when-then三段式结构组成,用于使用一组或多组数据来进行同种测试。

单元测试的工作:给出初始数据(given块),调用被测试的方法(when块),验证结果是否符合预期(then块)。

2.2.1 方法名

使用def来定义测试方法,方法名置于def之后、方法体之前,是一个双引号包起来的字符串,需要清晰明了地描述本测试行为或场景。

2.2.2 given块(数据块)

given块使用given : "given block description"来定义,其中块名/描述部分是可省略的。

given块用于设置测试的前置条件或初始化数据,跨行定义块内元素,不需要空两格。使用def来定义块内初始化数据,格式如def 变量名 = 变量值,使用的是弱类型,无需指明变量类型。如果变量需要跨测试方法共享,则需要使用@Shared注解标记。

 def "test some method"() { 
    given: "given block description" def localData = "local data" @Shared def sharedData = "shared data" // ... } 

2.2.3 when块(行动块)

when块的定义方式参考given块。

when块用于执行被测试的方法或操作,使用given块中的变量来触发待测试的行为得到结果,并将结果放入变量中。

 def "test some method"() { 
    // ... when: "when block description" // 以input为参数来调用testMethod()方法,并将结果放入result变量 def result = testMethod(input) // ... } 

2.2.4 then块(验证块)

then块的定义方式参考given块。

在then块中使用断言来验证when块中得到的结果是否符合预期。Spock提供了丰富的断言:

  • 比较值的断言:==!=><>=<=
  • 异常检查断言:thrown()notThrown()
  • 条件断言(组合多个断言使用):and&&or||not
  • 验证对象属性的断言(两个关键字):espectassert
  • 检查集合内容的断言:contains()containsAll()isEmpty()size()every(it > 0)

补充:上述断言中,有括号的都是方法,可以通过被检查的集合/对象点出来,需要在expect块中使用。

 def "test some method"() { 
    // ... then: "then block description" // 比较值的断言 result != "expected output" // 异常断言,验证是否抛出了指定异常 thrown(IllegalArgumentException) // 异常断言,验证是否没有抛出某异常 notThrown(NullPointerException) // 异常断言,验证是否抛出了某异常的子异常 def ex = thrown(Exception) ex instanceof IllegalArgumentException // 条件断言 num1 == 10 && num2 == -10 // assert关键字,后面跟一个布尔表达式 asset num == 10 expect: // 检查集合内容的断言 nums.contains(10) nums.every(it == 10) // ... } 

2.3 Spock类的生命周期方法

 def setupSpec() { 
    // 执行一次,整个Specification开始前 } def setup() { 
    // 每个测试方法执行前 } def cleanup() { 
    // 每个测试方法执行后 } def cleanupSpec() { 
    // 整个Specification结束后 } 

2.4 Mock和Stub

2.4.1 概述

Mock和Stub都可以模拟被测试代码的依赖对象,在作用上有所区别:

  • Mock:主要用于验证被测试代码与依赖对象之间的交互是否正确,包括方法调用的次数、顺序和参数等。同时,也可以提供固定的行为或数据。
  • Stub:主要用于替代依赖对象,提供固定的行为或数据,使得被测试的代码能够在已知条件下运行。

2.4.2 创建模拟对象(Mock和Stub都一样)

方式一:测试方法的given块中使用mock()方法 MyInterface dependencyMock = mock(MyInterface .class); 方式二:测试类成员变量+@Mock注解 @Mock private MyDependency myDependency; // 可以将前面Mock的对象自动注入到@InjectMocks标注的对象中 @InjectMocks private MyService myService; 

2.4.2 Mock对象

 // 设置返回值 when(dependencyMock.someMethod(param1)).thenReturn(someValue1) // 设置异常 doThrow(new PaymentException("Payment failed")).when(paymentGateway).charge(anyDouble()); ---------------------------- // 验证调用次数 verify(myDependency).method1(100.0);// 调用一次 verify(myDependency, never()).charge(anyDouble());// 从未调用 // 验证调用顺序 MyService myService = myService(dependency1, dependency2); myService.verify(dependency1).method1(); myService.verify(dependency2).method2(); 

2.4.3 Stub对象模拟行为

 // 预设方法返回固定值(下划线用于匹配任意参数,无需参数则不用使用) dependencyMock.someMethod(_) >> "mocked response" // 预设方法返回列表 dependencyMock.getListMethod(_) >> ["str1", "str2"] // 预设方法抛出异常 dependencyMock.anotherMethod(_) >> { 
    throw new IllegalArgumentException() } // 预设方法根据参数动态返回值 dependencyMock.methodWithArgs(_ as String, _ as Integer) >> { 
    String s, int i -> "$s - $i" } // 预设方法多次调用时的不同返回值 (1..3).each { 
    index -> dependencyMock.repeatedMethod(_) >> "response_$index" } // 使用closure模拟方法内部行为 dependencyMock.complexMethod(_) >> { 
    // 执行一些逻辑,返回结果 } 

2.4.1 Mock对象

 // 可以使用Mock()来获得指定类的Mock对象 def dependencyMock = Mock(DependencyClass) // 也可以使用Stub()来获得指定类的Mock对象 def dependencyStub = Stub(DependencyClass) 

2.4.3 Mock静态方法(以及final方法和private方法)

Spock基于动态代理实现,无法模拟静态方法、final方法和私有方法。但在Spock中可以通过powermock来模拟:(以下以静态方法为例)

  1. 加上@RunWith(PowerMockRunner.class)注解和@PowerMockRunnerDelegate(Sputnik.class)注解
  2. 加上@PrepareForTest(静态方法所在类.class)注解
  3. 在mock静态方法前调用PowerMockito.mockStatic(静态方法所在类.class)
  4. mock静态方法
  5. 调用被mock的静态方法
@RunWith(PowerMockRunner.class) @PowerMockRunnerDelegate(Sputnik.class) @PrepareForTest(ClassWithStaticMethod.class) def "test method using mocked static"() { 
    given: PowerMockito.mockStatic(ClassWithStaticMethod.class) 1 * ClassWithStaticMethod.staticMethod() >> "mocked result" when: def result = ClassWithStaticMethod.staticMethod() then: result == "mocked result" } 

2.5 使用where块实现数据驱动测试

2.5.1 where块(数据表格)的使用

在测试方法中使用where块定义一组或多组输入/输出数据,实现数据驱动测试(多组数据进行同种测试),每个数据行都会触发一次given-when-then流程。

使用where块进行驱动测试,需要按照一定格式来定义given-when-then,简而言之就是要“参数化”,在given块中的定义的变量的值要用参数(和变量不同,无需使用def定义),在when块中得到的结果值要用变量存储,在then块中的验证要基于上述的变量和参数来验证。完成given-when-then的“参数化”后,在where子句中以表格的方式设置输入/输出数据即即可。

 def "test some method"() { 
    // ... where: // 参数表(每条记录对应一组参数实例,都会进行一次单元测试) // 第一行为参数名组成的表头,之后每行对应一组参数实例 value | expected // 表头对应的数据行 "input1" | "output1" "input2" | "output2" } 

搭配@Unroll注解来进行数据驱动测试

不加@Unroll注解,那么会把所有的测试用例返回一个结果,即所有测试用例都通过了则显示通过,否则不通过,不通过时不会显示是具体哪条数据行没有通过。

通过在测试方法上加@Unroll,测试时会显示各个数据行的测试结果。每个数据行对应一个结果行,显示的是测试方法名。其中,测试方法名中可以使用#变量名来获取本次此时的数据,用于定位测试结果的数据行。

class IDNumberUtilsGroovyTest extends Specification { 
    @Unroll def "身份证号:#idNO 的生日,年龄是:#result"() { 
    expect: "执行以及结果验证" IDNumberUtils.getBirthAge(idNO) == result where: "数据驱动测试" idNO || result "" || ["birthday": "1992-08-09", "age": "29"] "" || ["birthday": "1993-08-09", "age": "28"] "" || ["birthday": "1994-08-09", "age": "27"] "" || ["birthday": "1995-08-09", "age": "26"] "" || ["birthday": "1996-08-09", "age": "25"] } } 

在这里插入图片描述

三、几个特殊场景下的测试Demo

3.1 测试异常信息

最笨的方法是在测试类中使用try-catch来测试异常信息,当设计多个异常时,无疑需要多个try-catch,使得代码不够简洁。

一般的做法是使用JUnit的ExpectedException方式,或者使用@Test(expected = BusinessException.class) 注解,但只能验证异常类型和异常信息,无法验证异常code。

这里,我们可以使用Spock内置的异常断言thrown()

  1. 在then块中使用异常断言捕获指定异常(不会中断测试程序):def exception = thrown(expectedException)
  2. 使用异常对象调用errorCode和errorMessage来获取code和massage,然后验证异常的code和massage。
 then: "捕获异常并设置需要验证的异常值" def exception = thrown(expectedException) // 参数化code和massage exception.errorCode == expectedErrCode exception.errorMessage == expectedMessage } 

3.2 测试DAO层

3.2.1 测试DAO层的特殊性

DAO层的测试不能使用Mock,否则无法验证SQL是否正确,需要去查询数据库测试。

最容易想到的方法是使用@SpringBootTest注解启动Spring应用上下文,从而可以获取Mybatis、Mapper实例进行数据库操作。这种方式很方便,但存在一定的缺点:

  • 启动上下文耗时长。
  • 若因为其他地方的问题导致启动失败,会导致无法进行DAO层的测试。
  • 需要到数据库尽可能隔离,避免污染数据,不同DAO层方法的测试之间相互影响。

3.2.2 测试DAO层的方案

(虽然很规范,但是很麻烦~我还是选@SpringBootTest

  1. 通过MyBatis的SqlSession启动mapper实例(避免通过Spring启动加载上下文信息)。
  2. 通过内存数据库(如H2)隔离大家的数据库连接(完全隔离不会存在互相干扰的现象)。
  3. 通过DBUnit工具,用作对于数据库层的操作访问工具。
  4. 通过扩展Spock的注解,提供对于数据库创建和数据Data加载的方式。

参考文章列表

写有价值的单元测试
通义千问
吃透单元测试:Spock单元测试框架的应用与实践
Spock单元测试框架介绍以及在美团优选的实践

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/144006.html

(0)
上一篇 2025-04-29 18:20
下一篇 2025-04-29 18:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信