單元測試
- Command + shift + T :創(chuàng)建類對應(yīng)的測試類
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 表示在每個測試類開始前執(zhí)行初始化操作
*/
@BeforeEach
public void init() {
//mock初始化
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
//header初始化,構(gòu)造請求頭
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;
}
}
細節(jié)圖








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代理類,測試類直接構(gòu)造想要的出參
//定義想要的理想返回值
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代理類,測試類直接構(gòu)造想要的出參
//定義想要的理想返回值
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) {
/**
* 模擬修改用戶的數(shù)據(jù)庫、rpc請求、發(fā)送郵件通知都未實現(xiàn)場景
*/
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 提供測試的一些自動配置。
我們只需要導(dǎo)入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不同,他的職責(zé)非常單一不能聲明任何屬性,拓展的測試將會由Jupiter提供額外測試
- @ParameterizedTest :表示方法是參數(shù)化測試,下方會有詳細介紹
- @RepeatedTest :表示方法可重復(fù)執(zhí)行,下方會有詳細介紹
- @DisplayName :為測試類或者測試方法設(shè)置展示名稱
- @BeforeEach :表示在每個單元測試之前執(zhí)行
- @AfterEach :表示在每個單元測試之后執(zhí)行
- @BeforeAll :表示在所有單元測試之前執(zhí)行
- @AfterAll :表示在所有單元測試之后執(zhí)行
- @Tag :表示單元測試類別,類似于JUnit4中的@Categories
- @Disabled :表示測試類或測試方法不執(zhí)行,類似于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 | 數(shù)組斷言 |
| assertAll | 組合斷言 |
| assertThrows | 異常斷言 |
| assertTimeout | 超時斷言 |
| fail | 快速失敗 |
4.2.3 嵌套測試
JUnit 5 可以通過 Java 中的內(nèi)部類和@Nested 注解實現(xiàn)嵌套測試,從而可以更好的把相關(guān)的測試方法組織在一起。在內(nèi)部類中可以使用@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 參數(shù)化測試
參數(shù)化測試是JUnit5很重要的一個新特性,它使得用不同的參數(shù)多次運行測試成為了可能,也為我們的單元測試帶來許多便利。
利用@ValueSource等注解,指定入?yún)ⅲ覀儗⒖梢允褂貌煌膮?shù)進行多次單元測試,而不需要每新增一個參數(shù)就新增一個單元測試,省去了很多冗余代碼。
@ValueSource: 為參數(shù)化測試指定入?yún)碓矗С职舜蠡A(chǔ)類以及String類型,Class類型
@NullSource: 表示為參數(shù)化測試提供一個null的入?yún)?/p>
@EnumSource: 表示為參數(shù)化測試提供一個枚舉入?yún)?/p>
@CsvFileSource:表示讀取指定CSV文件內(nèi)容作為參數(shù)化測試入?yún)?/p>
@MethodSource:表示讀取指定方法的返回值作為參數(shù)化測試入?yún)?注意方法返回需要是一個流)
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("參數(shù)化測試1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法來源參數(shù)")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
細節(jié)圖



