单元测试
单元测试
- Command + shift + T :创建类对应的测试类
Junit单元测试模板
Test
import com.alibaba.fastjson.JSON;
import com.bruce.boot.springbootdemo.SpringbootDemoApplication;
import com.bruce.boot.springbootdemo.bean.User;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import java.nio.charset.StandardCharsets;
@Slf4j
@SpringBootTest(classes = SpringbootDemoApplication.class)
class HelloControllerTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
private HttpHeaders httpHeaders;
private static final String BASIC_URL_PREFIX = "/boot/";
/**
* @BeforeEach 表示在每个测试类开始前执行初始化操作
*/
@BeforeEach
public void init() {
//mock初始化
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
//header初始化,构造请求头
LinkedMultiValueMap<String, String> headerMap = new LinkedMultiValueMap<>();
headerMap.add("Content-Type", "application/json");
headerMap.add("Accept", "application/json");
headerMap.add("Cookie", "sessionId=abc123");
headerMap.add("token", "sdafasdfff");
httpHeaders = new HttpHeaders();
httpHeaders.addAll(headerMap);
}
/**
* get请求示例
*
* @SneakyThrows:表示忽略异常
*/
@SneakyThrows
@Test
void hello() {
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get(BASIC_URL_PREFIX + "hello")
.headers(httpHeaders).contentType(MediaType.APPLICATION_JSON)
).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
String content = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
log.info("test content:{}", content);
}
@Test
void testHello() {
}
/**
* 测试header取值
*/
@SneakyThrows
@Test
void saveUser() {
User user = new User();
user.setName("bruce");
user.setId(23);
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(BASIC_URL_PREFIX + "saveUser")
.headers(httpHeaders)
.content(JSON.toJSONString(user))
.contentType(MediaType.APPLICATION_JSON)
).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
String content = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
log.info("test content:{}", content);
}
/**
*
*/
@SneakyThrows
@Test
void getHeader() {
User user = new User();
user.setName("bruce");
user.setId(23);
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post(BASIC_URL_PREFIX + "getHeader")
.headers(httpHeaders)
.content(JSON.toJSONString(user))
.contentType(MediaType.APPLICATION_JSON)
).andExpect(MockMvcResultMatchers.status().isOk()).andReturn();
String content = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8);
log.info("test content:{}", content);
}
}
Controller
@Slf4j
@RequestMapping("/boot")
@RestController
public class HelloController {
@Resource
private UserService userService;
@GetMapping("/hello")
public String hello(String name) {
for (int i = 0; i < 30; i++) {
log.info("info log name:{}", name);
log.error("error log name:{}", name);
}
log.info("info log name:{}", name);
log.error("error log name:{}", name);
return "hello " + name;
}
@GetMapping("/phone")
public Object hello() {
Phone iphone = Phone.builder().brand("iphone").size(20).type("15").build();
if (true) {
System.out.println("dd");
System.out.println("dd");
System.out.println("dd");
System.out.println("dd");
}
return iphone;
}
@GetMapping("/getUser")
public Object getUser() {
User user = userService.getUserById(1);
return user;
}
@PostMapping("/saveUser")
public Object saveUser(@RequestBody User user) {
return user;
}
@PostMapping("/getHeader")
public Object saveUser(@RequestHeader(name = "token") String token) {
return token;
}
}
细节图








Mockito单元测试
- 注意,使用@SpringbootTest时,要Mock的类要使用@MockBean注解进行代理,否则加载的会是容器中真实的实例。
单元测试类
package com.bruce.boot.springbootdemo.test;
import com.alibaba.fastjson.JSON;
import com.bruce.boot.springbootdemo.bean.User;
import com.bruce.boot.springbootdemo.controller.HelloController;
import com.bruce.boot.springbootdemo.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/***
* @ClassName: MockitoTest
* @Description:
* @author: lairongfeng
* @Date: 2024/8/27 01:12
* @version : 1.0
*/
@Slf4j
@SpringBootTest
@ExtendWith(SpringExtension.class)
public class MockitoTest {
/**
* 通过 @MockBean注解提前定义mock代理类
*/
@Resource
@MockBean
private UserService userService;
@Resource
private HelloController helloController;
/**
* 手动定义代理类
*/
@Test
public void updateUserTestManual() {
//定义mock代理类
userService = Mockito.mock(UserService.class);
userService.updateUer(Mockito.any());
}
@Test
public void updateUserTest() {
//使用@MockBean注解提前定义mock代理类,测试类直接构造想要的出参
//定义想要的理想返回值
Mockito.when(userService.updateUer(Mockito.any())).thenReturn(new User("aa", 1));
Mockito.when(userService.rpcRequest(Mockito.any())).thenReturn("OK");
Object user = helloController.updateUser(new User("bb", 2));
log.info(JSON.toJSONString(user));
}
@Test
public void rpcTest() {
//使用@MockBean注解提前定义mock代理类,测试类直接构造想要的出参
//定义想要的理想返回值
Mockito.when(userService.rpcRequest(Mockito.any())).thenReturn("OK");
String str = userService.rpcRequest(Mockito.any());
log.info(str);
}
}
@PostMapping("/updateUser")
public Object updateUser(@RequestBody User user) {
/**
* 模拟修改用户的数据库、rpc请求、发送邮件通知都未实现场景
*/
user = userService.updateUer(user);
System.out.println(userService.rpcRequest(user));
userService.sendEmail();
return user;
}
依赖
<!-- 添加 Mockito 依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.11.2</version>
<scope>test</scope>
</dependency>
springboot单元测试-JUnit5
4.1. 整合
SpringBoot 提供一系列测试工具集及注解方便我们进行测试。
spring-boot-test提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置。
我们只需要导入spring-boot-starter-test 即可整合测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test 默认提供了以下库供我们测试使用
4.2. 测试
4.2.0 组件测试
直接@Autowired容器中的组件进行测试
4.2.1 注解
JUnit5的注解与JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- @Test :表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- @ParameterizedTest :表示方法是参数化测试,下方会有详细介绍
- @RepeatedTest :表示方法可重复执行,下方会有详细介绍
- @DisplayName :为测试类或者测试方法设置展示名称
- @BeforeEach :表示在每个单元测试之前执行
- @AfterEach :表示在每个单元测试之后执行
- @BeforeAll :表示在所有单元测试之前执行
- @AfterAll :表示在所有单元测试之后执行
- @Tag :表示单元测试类别,类似于JUnit4中的@Categories
- @Disabled :表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- @Timeout :表示测试方法运行如果超过了指定时间将会返回错误
- @ExtendWith :为测试类或测试方法提供扩展类引用
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@DisplayName("😱")
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
4.2.2 断言
| 方法 | 说明 |
|---|---|
| assertEquals | 判断两个对象或两个原始类型是否相等 |
| assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
| assertSame | 判断两个对象引用是否指向同一个对象 |
| assertNotSame | 判断两个对象引用是否指向不同的对象 |
| assertTrue | 判断给定的布尔值是否为 true |
| assertFalse | 判断给定的布尔值是否为 false |
| assertNull | 判断给定的对象引用是否为 null |
| assertNotNull | 判断给定的对象引用是否不为 null |
| assertArrayEquals | 数组断言 |
| assertAll | 组合断言 |
| assertThrows | 异常断言 |
| assertTimeout | 超时断言 |
| fail | 快速失败 |
4.2.3 嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
4.2.4 参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用@ValueSource等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
细节图





浙公网安备 33010602011771号