2025/07/28

Spring Boot 3 unit test

libary 有兩個

  • 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>

測試還包含其他 library

  • JUnit 5

  • AssertJ

  • Hamcrest

  • Mockito

  • JSONassert

  • JsonPath


spring 提供一個 @SpringBootTest annotation,裡面有一個 webEnvironment 環境變數,支援以下設定

  • MOCK (default)

    載入一個 WebApplicationContext 並提供 Mock Web Environment。不會啟動內嵌的 web server,並可以結合 @AutoConfigureMockMvcor @AutoConfigureWebTestClient 進行 MOCK 測試

  • RANDOM_PORT

    載入一個 WebApplicationContext,提供一個真實的 web environment,隨機 port

  • DEFINED_PORT

    載入一個 WebApplicationContext,提供一個真實的 web environment,指定 port,預設 8080

  • NONE

    載入一個 WebApplicationContext,不提供 web environment


Spring MVC 測試

MvcTests.java

package com.test.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;

import java.util.HashMap;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MvcTests {

    @Test
    public void getUserTest(@Autowired TestRestTemplate testRestTemplate) {
        Map<String, String> multiValueMap = new HashMap<>();
        multiValueMap.put("username", "test1");
        Result result = testRestTemplate.getForObject("/user/get?username={username}",
                Result.class, multiValueMap);
        assertThat(result.getCode()).isEqualTo(0);
        assertThat(result.getMsg()).isEqualTo("ok");
    }

}

呼叫 /user/test,檢查回傳的資料是否正確

從 log 可發現 tomcat 運作在另一個 port

Tomcat started on port 53594 (http) with context path '/'

MOCK 測試

package com.test.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MockMvcTests {

    @Test
    public void getUserTest(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/user/get?username={username}", "test"))
                .andExpect(status().isOk())
                .andExpect(content().string("{\"code\":0,\"msg\":\"ok\",\"data\":\"test\"}"));
    }

}

在class 上面增加 @AutoConfigureMockMvc ,然後在 method 上加上 @Autowired MockMvc mvc 就可進行 MOCK 測試。

從 log 發現並沒有真的啟動一個 web server


MOCK 元件測試

package com.test.test;

import com.test.test.service.UserService;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class MockBeanTests {

//    @Autowired
//    private UserService userService;

    @MockitoBean
    private UserService userService;

    @Test
    public void countAllUsers() {
        BDDMockito.given(this.userService.countAllUsers()).willReturn(88);
        assertThat(this.userService.countAllUsers()).isEqualTo(88);
    }

}

@MockitoBean 代表該變數是被 Mock 覆蓋

這邊透過 BDDMockito 模擬 countAllUsers 回傳結果


JSON 測試

spring 提供各種技術元件的單元測試

想要測試 JSON,可使用 JsonTestersAutoConfiguration,只需要在測試類別加上 @JsonTest

先新增一個檔案 test/resources/jack.json

{"id":10001, "name":"Jack", "birthday": "2000-10-08 21:00:00"}

User POJO

package com.test.test;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
public class User {
    private long id;
    private String name;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;

}

JsonTests.java

package com.test.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

import java.time.LocalDateTime;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class JsonTests {

    @Autowired
    private JacksonTester<User> json;

    @Test
    void serialize() throws Exception {
        User user = new User(10001L, "Jack",
                LocalDateTime.of(2000, 10, 8, 21, 0, 0));
        assertThat(this.json.write(user)).isEqualToJson("/jack.json");
        assertThat(this.json.write(user)).hasJsonPathStringValue("@.name");
        assertThat(this.json.write(user)).
                extractingJsonPathStringValue("@.name").isEqualTo("Jack");
    }

    @Test
    void deserialize() throws Exception {
        String content = "{\"id\":10002, \"name\":\"Petty\", \"birthday\": \"2025-01-01 01:01:00\"}";
        assertThat(this.json.parse(content))
                .isEqualTo(new User(10002L, "Petty",
                        LocalDateTime.of(2025, 1, 1, 1, 1, 0)));
        assertThat(this.json.parseObject(content).getName()).isEqualTo("Petty");
    }

}

2025/07/21

Spring Boot 3 test

斷點測試

用 main 直接啟動 Debug mode

在啟動 application 時,用 Debug mode 方式啟動。當程式到達斷點時,會自動暫停

用 maven plugin

使用 main 啟動 debug mode 的缺點是,無法使用 maven plugins。

使用 spring boot maven plugin 啟動 application 會 fork process 執行,故無法像 main 的方式一樣,直接 debug。需要用遠端測試的方法。

首先是需要在 spring boot maven plugin 中,設定 jvmArguments 測試參數,並啟動 application,然後新增一個遠端測試

啟動測試模式

有兩種方法

  • 在 plugin 設定中指定測試模式的 jvmArguments

        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <jvmArguments>
                            -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005
                        </jvmArguments>
                    </configuration>
                </plugin>
            </plugins>
        </build>
  • 使用 mvn 啟動時,指定測試參數

    mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"

啟動後,會在 console 最後面看到

> mvn spring-boot:run
[INFO] --- spring-boot-maven-plugin:3.4.0:run (default-cli) @ test ---
[INFO] Attaching agents: []
Listening for transport dt_socket at address: 5005

遠端測試

透過 IDE 建立 Remote test application

連接後,剛剛的 mvn 才會完成啟動,出現 application 的 log

Connected to the target VM, address: 'localhost:5005', transport: 'socket'

JVM remote debug 要處理斷點會比較麻煩


開發者工具

spring boot 提供 devtools,功能可自動禁用 cache,支援 application 自動重啟,即時 reload

只要引用 libary 即可

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

optional 代表只會在本專案/子專案中使用,不會傳遞到引用該專案的project

devtools 是在開發時使用,打包時 spring boot 預設不會把 devtools 包裝進去。可修改此設定

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludeDevtools>false</excludeDevtools>
                </configuration>
            </plugin>
        </plugins>

並在啟動時,加上 -Dspring.devtools.restart.enabled=true 啟用

但不建議將 devtools 打包進去


預設值

Name Default Value
server.error.include-binding-errors always
server.error.include-message always
server.error.include-stacktrace always
server.servlet.jsp.init-parameters.development true
server.servlet.session.persistent true
spring.docker.compose.readiness.wait only-if-started
spring.freemarker.cache false
spring.graphql.graphiql.enabled true
spring.groovy.template.cache false
spring.h2.console.enabled true
spring.mustache.servlet.cache false
spring.mvc.log-resolved-exception true
spring.reactor.netty.shutdown-quiet-period 0s
spring.template.provider.cache false
spring.thymeleaf.cache false
spring.web.resources.cache.period 0
spring.web.resources.chain.cache false

devtools 預設禁用 cache,因為開發階段禁用 cache,較容易排除問題

可透過設定修改

spring:
  devtools:
    add-properties: false

自動 restart

devtools 會監控檔案,發生變更就會快速自動 restart。這邊使用兩個 class loader

  • base classloader

    基本不會變更的檔案,ex: 3rd party jar

  • restart classloader

    application 相關檔案

restart 時,base classloader 不需要異動,只需要重建一個 restart classloader

但要注意:AspectJ AOP programming 不支援 auto restart


觸發機制

  • Eclipse: 修改 save 後就會觸發

  • IntelliJ IDEA: 要重新 build 才會觸發

  • build tool: 在 IDE 使用 mvn compile/grdle build 會觸發


修改時頻繁 restart,會消耗很多資源

可將 IDE 改為手動編譯,或是指定一個觸發檔案

spring:
  devtools:
    restart:
      trigger-file: .reloadtrigger

只有在 .reloadtrigger 檔案被修改時,才會自動 restart


IDE 有支援自動產生 trigger file

然後在 application 有個 update 按鈕

此方法只適用於用 main 啟動的 spring application,不適用 mvn


排除資源

預設狀況下以下資源目錄不會觸發 restart,但會觸發自動 reload

  • /META-INF/maven

  • /META-INF/resources

  • /resources

  • /static

  • /public

  • /templates

可修改此設定

spring:
  devtools:
    restart:
      exclude: static/**,public/**
      # 保留預設,增加其他目錄
      # additional-exclude:

禁用 log

application 自動 restart 都會列印 log,可用以下方式關閉

spring:
  devtools:
    restart:
      log-confition-evaluation-delta: false

禁用 auto restart

spring:
  devtools:
    restart:
      enabled: false

auto reload

browser 要安裝 LiveReload plugin,並打開 devtool 的 auto reload 功能

devtools 內建一個 LiveReload server 在 port 35729

預設狀況下以下資源目錄不會觸發 restart,但會觸發自動 reload

  • /META-INF/maven

  • /META-INF/resources

  • /resources

  • /static

  • /public

  • /templates

可關閉 auto reload

spring:
  devtools:
    livereload:
      enabled: false

global 設定

$HOME/.config/spring-boot 目錄中,設定檔名為 spring-boot-devtools。支援 properties 或 yaml

spring-boot-devtools.properties

spring-boot-devtools.yaml

spring-boot-devtools.yml

$HOME 預設是 user 主目錄,可用以下方式修改

  • 設定 SPRING_DEVTOOLS_HOME 環境變數

  • 設定 spring-devtools.home 系統參數

example: spring-boot-devtools.yml

spring:
  devtools:
    restart:
      log-condition-evaluation-delta: false
      trigger-file: ".reloadtrigger"