大家好,欢迎来到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
- 验证对象属性的断言(两个关键字):
espect
,assert
- 检查集合内容的断言:
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来模拟:(以下以静态方法为例)
- 加上
@RunWith(PowerMockRunner.class)
注解和@PowerMockRunnerDelegate(Sputnik.class)
注解 - 加上
@PrepareForTest(静态方法所在类.class)
注解 - 在mock静态方法前调用
PowerMockito.mockStatic(静态方法所在类.class)
- mock静态方法
- 调用被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()
:
- 在then块中使用异常断言捕获指定异常(不会中断测试程序):
def exception = thrown(expectedException)
。 - 使用异常对象调用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
)
- 通过MyBatis的SqlSession启动mapper实例(避免通过Spring启动加载上下文信息)。
- 通过内存数据库(如H2)隔离大家的数据库连接(完全隔离不会存在互相干扰的现象)。
- 通过DBUnit工具,用作对于数据库层的操作访问工具。
- 通过扩展Spock的注解,提供对于数据库创建和数据Data加载的方式。
参考文章列表
写有价值的单元测试
通义千问
吃透单元测试:Spock单元测试框架的应用与实践
Spock单元测试框架介绍以及在美团优选的实践
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/144006.html