单元测试的目的
避免大量人工参与,将测试流程通过脚本、策略、框架来实现自动化测试
为什么要写单元测试
测试一定是在你最担心的地方测试,而不是为了追求测试而去做测试
单元测试是工具而非目标
核心价值:缩短反馈周期、降低缺陷修复成本
如上左图,BUG出现最多的是本地开发阶段,之后的单元测试、上线后bug的
单侧对组织的价值

单侧对个人的价值

单元测试框架

Mock介绍
Mock的作用如下
模拟复杂业务的接口 : 实际工作中如果我们在测试一个接口功能时,如果这个接口依赖个非常复杂的接口业务或者来源于第三方接口 ,那么我们完全可以使用Mock来模拟这个复杂的业务接口,其实这个和解决接口依赖是一样的原理。
前后端联调: 进行前后端分离编程时,如果进行一个前端页面开发,需要根据后台返回的状态展示不同的页面,那么就需要调用后台的接口,但是后台接口还未开发完成,完全可以借助mock来模拟后台这个接口返回想要的数据。
Mock 与Stub(桩)
桩代码(Stub) :用来代替真实代码的临时代码,主要作用是使被测代码能够独立编译链接,并独立运行;
Mock 代码:也是用来代替真实代码的临时代码,起到隔离和补齐的作用,但是它还可深入的模拟对象之间的交互方式,可以对结果进行验证。
认识Mockito
-
Mockito是Java单元测试中使用率最高的Mock框架之一
-
它通过简明的语法和完整的文档吸引了大量的开发者。Mockito支持用Maven和Gradle来进行依赖引入和管理。
Mockito Maven 引入
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.4.6</version>
<type>pom</type>
</dependency>
验证指定行为
Mockito 可以验证某些行为:一旦mock对象被创建了mock对象会记住所有的交互。
public class PersonTest {
// 定义一个测试用例叫verifyTest
@Test
void verifyTest(){
Person mockPerson=mock(Person.class);
mockPerson.setId(1);
verify(mockPerson).setId(1);
verify(mockPerson).setName("tom");
}
}
运行测试用例

错误信息显示你期望person.setName("tom")被调用,但实际只有person.setId(1)被调用了。
**错误原因:**
-
你使用
verify(person).setName("tom")来验证setName方法是否被调用 -
但实际测试代码中只调用了
person.setId(1),没有调用setName方法 -
Mockito严格检查方法调用,发现预期与实际不符
在测试用例中补充mockPerson.setName("tom");
public class PersonTest {
// 定义一个测试用例叫verifyTest
@Test
void verifyTest(){
Person mockPerson=mock(Person.class);
mockPerson.setId(1);
mockPerson.setName("tom");
verify(mockPerson).setId(1);
verify(mockPerson).setName("tom");
}
}
指定返回值
Mockito 可以做一些测试桩(Stub):默认情况下,所有的函数都有返回值,mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;
public class PersonTest {
// 定义第二个测试用例
@Test
void stubTest(){
Person mockPerson=mock(Person.class);
when(mockPerson.getId()).thenReturn(1);
// 定义一个异常返回
when(mockPerson.getName()).thenThrow(new RuntimeException());
// 结果会打印出1
System.out.println(mockPerson.getId());
}
}
由于我们定义了mockPerson一个行为(调用getName()方法会抛出异常),但是并没有去调用getName()方法,因此运行时不会报错

当我们模拟调用getName()方法时就会报错
public class PersonTest {
// 定义第二个测试用例
@Test
void stubTest(){
Person mockPerson=mock(Person.class);
when(mockPerson.getId()).thenReturn(1);
// 定义一个异常返回
when(mockPerson.getName()).thenThrow(new RuntimeException());
// 结果会打印出1
System.out.println(mockPerson.getId());
System.out.println(mockPerson.getName());
}
}
匹配器
Mockito 以自然的java风格来验证参数值:为了合理的使用复杂的参数匹配,使用equals0)与anyX()的匹配器会使得测试代码更简洁、简单。
public class PersonTest {
@Test
void matchTest(){
Person mockPerson=mock(Person.class);
// 通过anyString()表示匹配任何一个String类型的值,最后我都会返回hello
when(mockPerson.setKey(anyString())).thenReturn("hello");
System.out.println(mockPerson.setKey("10"));
// 验证setKey方法是否被调用
verify(mockPerson).setKey(anyString());
}
}

无论传递什么值最后都会输出hello
且 verify(mockPerson).setKey(anyString());验证并未出错,表示方法被正常调用
验证调用次数
Mockito 可以验证函数的确切、最少、从未调用次数:verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。
@Test
void timesTest(){
Person mockPerson=mock(Person.class);
mockPerson.setId(1);
mockPerson.setName("tom");
mockPerson.setName("tom");
// 验证setId方法是否被调用1次
verify(mockPerson).setId(1);
// 验证.setName方法是否被调用2次
verify(mockPerson,times(2)).setName("tom");
}
验证执行顺序
Mockito 可以验证验证执行执行顺序:验证执行顺序是非常灵活的,不需要一个一个的验证所有交互,只需要验证感兴趣的对象即可。另外,可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。
@Test
void orderTest(){
Person singleMockPerson=mock(Person.class);
singleMockPerson.setName("jack");
singleMockPerson.setName("tom");
InOrder inOrder = inOrder(singleMockPerson);
// 验证先执行的 singleMockPerson.setName("jack")
// 后执行的 singleMockPerson.setName("tom");
inOrder.verify(singleMockPerson).setName("jack");
inOrder.verify(singleMockPerson).setName("tom");
Person firstMockPerson=mock(Person.class);
Person secondMockPerson=mock(Person.class);
firstMockPerson.setId(1);
secondMockPerson.setId(2);
InOrder inOrder1 = inOrder(firstMockPerson,secondMockPerson);
// 验证先执行的secondMockPerson.setId(2)
// 后执行的 firstMockPerson.setId(1);
inOrder1.verify(secondMockPerson).setId(2);
inOrder1.verify(firstMockPerson).setId(1);
}
模拟连续调用
Mockito 可以为连续的调用做测试桩(stub):有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。
@Test
void consStubTest(){
Person mockPerson=mock(Person.class);
when(mockPerson.getName())
// 模拟第一次调用时返回jack
.thenReturn("jack")
// 模拟第二次调用时返回tom
.thenReturn("tom");
// 第一次调用,返回jack
System.out.println(mockPerson.getName());
// 第二次调用,返回tom
System.out.println(mockPerson.getName());
}
SpringBoot中Mockito应用
-
从 Spring Boot 项目结构上来说,Service 层是依赖 Dao 层的,而Controller 层又依赖于 Service 层
-
从单元测试角度,对某个 Service 和 Controler 进行单元的时候,他所有依赖的类都应该进行Mock;而 Dao 层单元测试就比较简单了,只依赖数据库中的数据。
@SpringBootTest 是 Spring Boot 框架中用于集成测试的核心注解,它能够加载完整的 Spring 应用上下文,模拟真实的生产环境来验证业务逻辑和组件交互
使用@Mock注解来Mock对象时的第一种实现,即使用MockitoAnnotations.initMocks(testClass)
package com.example.demo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import static org.mockito.Mockito.*;
// springboot的单元测试用例集合
@SpringBootTest
public class TokenServiceTest {
@Mock
private TokenService tokenService;
// 在每个测试用例执行前
@BeforeEach
public void before(){
MockitoAnnotations.initMocks(this);
}
@Test
public void test01(){
Token token = new Token(1, "test");
when(tokenService.getTokenById(1)).thenReturn(token);
System.out.println(tokenService.getTokenById(1).getToken());
verify(tokenService).getTokenById(1);
}
}
