單元測試

單元測試

  • 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é)圖

image.png
image.png

image.png

image.png

image.png

image.png

image.png

image.png

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. 測試

測試時可以參考官網(wǎng)寫法

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 斷言

斷言官網(wǎng)寫法參考

方法 說明
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é)圖

image.png

image.png

image.png

image.png

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容