什么是单元测试
单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误。
单元测试的价值
相关框架:
-
测试框架:测试框架是一种工具或库,用于编写、组织和运行测试用例。它提供了一组规则和结构,帮助开发人员有效地进行单元测试、集成测试等各种测试。
-
断言框架:断言框架用于编写测试用例中的断言,即验证代码执行结果是否符合预期。它提供了丰富的断言方法,用于比较、判断和验证预期结果。
-
MOCK框架:用于模拟系统中的组件,定义模拟对象的行为,并验证代码与模拟对象的交互。
相关依赖:
1.Spring Boot Starter Test – JUnit5:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
- Spring Boot Starter Test 包含了一些用于测试 Spring Boot 应用程序的依赖项,包括 TestNG 和 JUnit 的支持。
- 已经引入Spring Boot Starter Test时,无需单独引入Junit5依赖
2.AssertJ :
<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
- AssertJ 是一个流式断言库,用于编写更具可读性和表达性的断言。
- AssertJ相对JUnit本身的语法更加清晰、流畅,使断言更易读。
- AssertJ 提供了更多的断言选项,覆盖了更多的测试场景。
- AssertJ 支持链式调用,可以在一个断言中完成多个验证。
3.Mockito:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy-agent -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.5</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
- Mockito是一个用于Java的开源Mocking(模拟)框架。它允许你在测试中创建和使用模拟对象,从而轻松模拟和控制对象的行为。
- Mockito旨在提供简洁、直观的API,使得编写测试更加容易。
###4.Testable:
<dependency>
<groupId>com.alibaba.testable</groupId>
<artifactId>testable-all</artifactId>
<version>${testable.version}</version>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-javaagent:${settings.localRepository}/com/alibaba/testable/testable-agent/0.7.9/testable-agent-0.7.9.jar</argLine>
</configuration>
</plugin>
- 单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上。
- 无需初始化,不挑服务框架,甭管要换的是私有方法、静态方法、构造方法还是其他任何类的任何方法,也甭管要换的对象是怎么创建的。写好Mock定义,加个@MockInvoke注解,一切统统搞定。
JUnit5:
import org.junit.jupiter.api.*;
/**
* 功能描述: JUnit5测试类;
*
* @Author: wangxu
* @Date: 2023/12/15 13:18
*/
public class JUnit5Test {
@BeforeAll
static void before() {
System.out.println("=====BeforeAll=====");
}
@AfterAll
static void after() {
System.out.println("=====AfterAll=====");
}
@BeforeEach
void beforeEach() {
System.out.println("=====BeforeEach=====");
}
@AfterEach
void afterEach() {
System.out.println("=====AfterEach=====");
}
@org.junit.jupiter.api.Test
@DisplayName("简单测试")
void testSimple() {
System.out.println("第一个测试方法");
}
@Disabled
@org.junit.jupiter.api.Test
@DisplayName("禁用测试")
public void disabledTest() {
//这个测试不会运行
System.out.println("这个测试不会执行");
}
@DisplayName("重复测试")
@RepeatedTest(value = 3)
public void i_am_a_repeated_test() {
System.out.println("执行测试");
}
@Nested
@DisplayName("第一个内嵌测试类")
class FirstNestTest {
@org.junit.jupiter.api.Test
void test() {
System.out.println("第一个内嵌测试类执行测试");
}
}
@Nested
@DisplayName("第二个内嵌测试类")
class SecondNestTest {
@org.junit.jupiter.api.Test
void test() {
System.out.println("第二个内嵌测试类执行测试");
}
}
}
AssertJ:
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言
import org.junit.jupiter.api.Test;
import java.util.*;
import static org.assertj.core.api.Assertions.assertThat;
// 使用 AssertJ的断言
public class AssertJSarnpleTest {
@Test
public void testUsingAssertJ() {
// 子符串判断
String s = "abcde";
assertThat(s).as("字符串判断,判断首尾及长度").startsWith("ab").endsWith("de").hasSize(5);
// 数字判断
Integer i = 50;
assertThat(i).as("数字判断,数字大小比较").isGreaterThan(10).isLessThan(100);
// 日期判断
Date date1 = new Date();
Date date2 = new Date(date1.getTime() + 100);
Date date3 = new Date(date1.getTime() - 100);
assertThat(date1).as("日期判断:日期大小比较").isBefore(date2).isAfter(date3);
// list比较
List<String> list = Arrays.asList("a", "b", "c", "d");
assertThat(list).as("list的首尾元素及长度").startsWith("a").endsWith("d").hasSize(4);
// Map判断
Map<String, Object> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
assertThat(map).as("Map的长度及键值测试").hasSize(3).containsKeys("A", "B", "C");
}
}
Mockito:
import com.hzsun.core.common.exception.BusinessException;
import com.hzsun.uc.UserCenterStarter;
import com.hzsun.uc.pojo.response.UserInfoResponse;
import com.hzsun.uc.service.impl.UcUserServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import java.util.Random;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
/**
* 功能描述: ;
*
* @Author: wangxu
* @Date: 2023/12/18 9:38
*/
//指定springboot的启动入口,模拟一个服务器环境,不然会在初始化websocket时报错
@SpringBootTest(classes = UserCenterStarter.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MockitoTest {
@SpyBean
private UcUserServiceImpl userServiceSpy;
@MockBean
private UcUserServiceImpl userServiceMock;
@Mock
private Random randomMock;
@Spy
private Random randomSpy;
@BeforeEach
void each(){
MockitoAnnotations.openMocks(this);
}
@Test
public void mock_normal_class(){
assertThat(randomMock.nextInt()).as("mock注解会默认模拟为这个类型的默认值,如int就模拟为0:").isEqualTo(0);
assertThat(randomSpy.nextInt()).as("spy对象,它默认会调用原始方法,除非显式地对方法进行了打桩");
when(randomSpy.nextInt()).thenReturn(10).thenReturn(20).thenThrow(new BusinessException("只能调用2次"));
assertThat(randomSpy.nextInt()).as("spy注解打桩后会走mock的方法").isEqualTo(10);
assertThat(randomSpy.nextInt()).as("spy注解打桩后会走mock的方法").isEqualTo(20);
// assertThat(randomSpy.nextInt()).as("spy注解打桩后会走mock的方法").isEqualTo(30);
verify(randomSpy,times(3)).nextInt();
}
@Test
public void mock_spirng_bean() {
UserInfoResponse oneById = userServiceMock.getOneById(1);
assertThat(oneById).isNull();
UserInfoResponse userInfoResponse = new UserInfoResponse();
userInfoResponse.setId(1);
userInfoResponse.setUserName("王旭");
userInfoResponse.setSex(1);
userInfoResponse.setBirthday("1998-10-04");
userInfoResponse.setUserNumber("123456789");
when(userServiceMock.getOneById(1)).thenReturn(userInfoResponse).thenReturn(userInfoResponse);
assertThat(userServiceMock.getOneById(1)).extracting(UserInfoResponse::getUserName).isEqualTo("王旭");
assertThat(userServiceMock.getOneById(1)).hasFieldOrPropertyWithValue("sex",1);
}
}
#Testable
package com.hzsun.uc.test;
import cn.hutool.core.collection.CollUtil;
import com.hzsun.uc.service.IUcTnUserClassService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
/**
* 功能描述: 主要介绍如何mock私有方法和spring中bean的模拟;
*
* @Author: wangxu
* @Date: 2023/12/18 16:37
*/
@Component
@Slf4j
public class Testable {
@Autowired
private IUcTnUserClassService tnUserClassService;
public void replaceUserClasses(Integer userId, Integer userMainClassId, Set<Integer> userClassIds) {
Set<Integer> classIdSet = new HashSet<>();
classIdSet.add(userMainClassId);
if (CollUtil.isNotEmpty(userClassIds)) {
classIdSet.addAll(userClassIds);
}
log.info("replaceUserClasses被调用");
tnUserClassService.replaceAll(userId, classIdSet);
}
}
package com.hzsun.uc.test;
import com.alibaba.testable.core.annotation.MockInvoke;
import com.alibaba.testable.core.model.MockScope;
import com.hzsun.uc.UserCenterStarter;
import com.hzsun.uc.service.IUcTnUserClassService;
import com.hzsun.uc.service.impl.UcTnUserClassServiceImpl;
import com.hzsun.uc.test.Testable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import static com.alibaba.testable.core.matcher.InvocationVerifier.verifyInvoked;
import static com.alibaba.testable.core.tool.TestableTool.MOCK_CONTEXT;
import static com.alibaba.testable.core.tool.TestableTool.SOURCE_METHOD;
/**
* 功能描述: 主要介绍如何mock私有方法和spring中bean的模拟;
*
* @Author: wangxu
* @Date: 2023/12/18 16:37
*/
public class TestableTest {
private Testable testable = new Testable();
public static class Mock{
@MockInvoke(targetClass = IUcTnUserClassService.class)
private void replaceAll(Integer userId, Collection<Integer> classIds){
System.out.println("replaceAll被调用了");
}
}
@Test
public void mock_spirng_bean() {
testable.replaceUserClasses(1,1,new HashSet<>());
Set<Integer> userMainClassIdSet = new HashSet<>();
userMainClassIdSet.add(1);
testable.replaceUserClasses(1,1,userMainClassIdSet);
}
}
规范:
遵循 F.I.R.S.T 原则:
- Fast(快速): 单元测试应该非常迅速执行。
- Independent(独立): 单个测试不应该依赖于其他测试的执行结果。对于外部依赖(例如数据库、网络调用),使用模拟(Mock)来隔离测试。
- Repeatable(可重复): 在任何环境中,测试都应该产生相同的结果。
- Self-Validating(自验证): 测试应该有一个布尔输出,测试通过则返回 true,否则返回 false。
- Timely(及时): 最好在编写实际代码之前编写测试,而不是等到实现完成后再写。
单测包结构
1、test包结构包名必须同main一致
2、对应类的单元测试类命名:${className}Test,
eg: main:com.hellobike.ride.application.RideIfaceImpl ===> test:com.hellobike.ride.application.RideIfaceImplTest
为测试使用清晰和描述性的命名:
测试方法应该清晰地描述正在测试的场景或功能。使用命名约定,例如 replaceUserClasses方法的测试方法为replaceUserClasses_nonEmptyUserClassIds和replaceUserClasses_emptyUserClassIds
使用JaCoCo进行类覆盖率、方法覆盖率、行覆盖率的统计。行覆盖率需达到?
写可维护的测试:
单元测试是可维护性的一部分。当代码发生变化时,确保测试也相应地进行更新。
覆盖关键路径和边界情况、异常情况:
确保测试覆盖主要路径,并考虑在输入边界情况/异常情况下进行测试。
定期运行测试:
将测试集成到持续集成(CI)流程中,以确保每次代码更改时都运行测试。