Published on

「Spring Boot API 開發:從 0 到 1」Day 29 SpringBootTest 測試

在前面的文章中,我們已經測試了 Service 、Repository 和 Controller 的部分

今天要來探討,如何在 Spring Boot 裡面實作端對端測試

SpringBootTest 簡介

SpringBootTest 是 Spring Boot 提供的一個強大的測試工具

它讓我們能在測試環境中啟動完整的 Spring 應用程式環境

這意味著我們可以測試整個應用程式的行為,從控制器到資料庫,就像在真實環境中運行一樣

主要用途

  • 整合測試:測試多個組件如何協同工作
  • 端對端測試:模擬真實的 HTTP 請求並驗證回應
  • 測試完整的應用程式配置:確保所有 Bean 都正確配置和注入

限制

  • 啟動時間較長:由於需要啟動完整的應用程式,測試啟動時間可能會較長
  • 資源消耗:相比單元測試,整合測試 和端對端測試 會消耗更多的系統資源
  • 測試隔離:需要特別注意測試之間的`資料隔離`,以避免測試間相互影響

端對端測試

測試 properties 檔案

首先,在 src/test 下建立 resources 目錄,並在其中建立 application-test.properties 檔案

# 使用記憶體資料庫
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# 指定 JPA 使用的資料庫語言,這告訴 Hibernate 使用 H2 特定的 SQL 語法
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# 在應用程式啟動時自動建立資料庫結構(表格等),並在應用程式關閉時自動刪除
spring.jpa.hibernate.ddl-auto=create-drop

在 test 的 package 下面,建立 TodoEndToEndTest  測試類別

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
public class TodoEndToEndTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private TodoRepository todoRepository;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    void setUp() {
        todoRepository.deleteAll();
    }

    @Test
    void createTodo() {

        // 呼叫 API
        // 這裡示範了怎麼使用完整 url (port) 來呼叫
        // 在使用 TestRestTemplate 就可以不用指定主機和 port 號,這裡只是示範
        String url = "http://localhost:" + port + "/api/todos";
        Todo newTodo = new Todo(null, "測試待辦事項", false);
        var response = restTemplate.postForEntity(url, newTodo, MyApiResponse.class);

        // 驗證回應資料
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertTrue(response.getBody().isSuccess());

        Todo responseTodo = objectMapper.convertValue(response.getBody().getData(), new TypeReference<>() {});
        assertEquals("測試待辦事項", responseTodo.getTitle());
        assertFalse(responseTodo.isCompleted());

        // 驗證資料庫資料
        List<Todo> todos = todoRepository.findAll();
        assertEquals(1, todos.size());
        assertEquals("測試待辦事項", todos.get(0).getTitle());
        assertFalse(todos.get(0).isCompleted());
    }

    @Test
    void getAllTodos() {

        // 新增測試資料
        Todo todo1 = new Todo(null, "測試待辦事項", false);
        Todo todo2 = new Todo(null, "測試待辦事項2", true);
        todoRepository.saveAll(Arrays.asList(todo1, todo2));

        // 呼叫 API
        var response = restTemplate.getForEntity("/api/todos", MyApiResponse.class);

        // 驗證回應資料
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertTrue(response.getBody().isSuccess());

        List<Todo> todos = objectMapper.convertValue(response.getBody().getData(), new TypeReference<>() {});

        assertEquals(2, todos.size());
        assertEquals("測試待辦事項", todos.get(0).getTitle());
        assertFalse(todos.get(0).isCompleted());
        assertEquals("測試待辦事項2", todos.get(1).getTitle());
        assertTrue(todos.get(1).isCompleted());
    }

    @Test
    void getTodo() {

        // 新增測試資料
        Todo todo = todoRepository.save(new Todo(null, "測試待辦事項", false));

        // 呼叫 API
        var response = restTemplate.getForEntity("/api/todos/" + todo.getId(), MyApiResponse.class);

        // 驗證回應資料
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertTrue(response.getBody().isSuccess());

        Todo responseTodo = objectMapper.convertValue(response.getBody().getData(), new TypeReference<>() {});
        assertEquals("測試待辦事項", responseTodo.getTitle());
        assertFalse(responseTodo.isCompleted());
    }

    @Test
    void updateTodo() {

        // 新增測試資料
        Todo todo = todoRepository.save(new Todo(null, "原始待辦事項", false));

        // 呼叫 API
        Todo updatedTodo = new Todo(todo.getId(), "更新後的待辦事項", true);
        restTemplate.put("/api/todos/" + todo.getId(), updatedTodo);

        // 驗證資料庫資料
        Todo actualTodo = todoRepository.findById(todo.getId()).get();
        assertEquals("更新後的待辦事項", actualTodo.getTitle());
        assertTrue(actualTodo.isCompleted());
    }

    @Test
    void testDeleteTodo() {

        // 新增測試資料
        Todo savedTodo = todoRepository.save(new Todo(null, "要刪除的待辦事項", false));

        // 呼叫 API
        restTemplate.delete("/api/todos/" + savedTodo.getId());

        // 驗證資料庫資料
        List<Todo> todos = todoRepository.findAll();
        assertEquals(0, todos.size());
    }
}
  • @SpringBootTest 啟動完整的應用程式
    • 使用 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT 時,Spring Boot 會啟動一個測試用的應用程式,並使用一個隨機的可用 port 號
  • @TestPropertySource 指定使用測試專用的屬性文件
  • @LocalServerPort 可以獲取測試用應用程式隨機分配的 port 號
  • TestRestTemplate,它是 Spring Boot 提供的用於進行 HTTP 請求的測試工具
    • TestRestTemplate 會自動配置為使用正確的主機和 port 號,所以在大多數情況下,都不需要顯式的使用 @LocalServerPort
  • 在取得 response 裡面的 data 時,使用了 objectMapper 來轉換成 Todo 相關的物件

結論

SpringBootTest 為我們提供了一個強大的工具,用於進行整合測試 和 端對端測試

透過這種方式,我們可以確保整個應用程式的各個元件能夠正確地協同運作

雖然這種測試相比單元測試更耗時且消耗更多資源,但它能夠捕捉到單元測試可能遺漏的問題,特別是在組件集成方面的問題

在實際開發中,我們應該結合單元測試和端對端測試,以確保應用程式的品質和可靠性

同步刊登於 iTHome 鐵人賽 「Spring Boot API 開發:從 0 到 1」Day 29 SpringBootTest 測試

圖片來源:AI 產生

參考連結