
因為加入了 Spring Security,所以我們需要調整相關的測試程式碼,來加入相關的認證和授權
這是最常用的註解之一,用於模擬一個已認證的使用者
你可以指定使用者名稱、角色,甚至是權限
@Test
@WithMockUser(username = "user", roles = "USER")
// @WithMockUser(username = "admin", roles = {"USER", "ADMIN"})  
public void testUserAccess() {
    // 測試程式碼
}
用於模擬匿名使用者:
@Test
@WithAnonymousUser
public void testAnonymousAccess() {
    // 測試程式碼
}
如果你有自訂的 UserDetailsService,可以使用這個註解
@Test
@WithUserDetails("user@example.com")
public void testWithUserDetails() {
    // 測試程式碼
}
這個類別提供了一些有用的方法,特別是在使用 MockMvc 進行測試時
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
這裡的 csrf() 方法會為請求添加一個有效的 CSRF token 
@Test
public void testWithCsrf() throws Exception {
    mockMvc.perform(post("/api/data").with(csrf()))
            .andExpect(status().isOk());
}
RequestPostProcessor 模擬使用者除了使用註解,你也可以在特定的請求中模擬使用者
mockMvc.perform(get("/api/data").with(user("admin").roles("ADMIN")))
        .andExpect(status().isOk());
在每個測試方法上加上 @WithMockUser 註解,模擬一個已登入的使用者。
在 POST、PUT 和 DELETE 請求中,增加 .with(csrf()) 來模擬 CSRF token 
@WebMvcTest(TodoController.class)
public class TodoControllerTest {
    @Test
    @WithMockUser(username = "user")
    public void createTodo() throws Exception {
        Todo todo = new Todo(null, "新待辦事項", false);
        Todo savedTodo = new Todo(1L, "新待辦事項", false);
        when(todoService.save(any(Todo.class))).thenReturn(savedTodo);
        mockMvc.perform(post("/api/todos")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(todo)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data.id").value(1))
                .andExpect(jsonPath("$.data.title").value("新待辦事項"))
                .andExpect(jsonPath("$.data.completed").value(false));
        verify(todoService, times(1)).save(any(Todo.class));
    }
    @Test
    @WithMockUser(username = "user")
    public void getAllTodos() throws Exception {
        // 測試內容保持不變
    }
    @Test
    @WithMockUser(username = "user")
    public void getTodo() throws Exception {
        // 測試內容保持不變
    }
    @Test
    @WithMockUser(username = "user")
    public void updateTodo() throws Exception {
        Todo updatedTodo = new Todo(1L, "更新的待辦事項", true);
        when(todoService.updateTodo(eq(1L), any(Todo.class))).thenReturn(Optional.of(updatedTodo));
        mockMvc.perform(put("/api/todos/1")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updatedTodo)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.data.id").value(1))
                .andExpect(jsonPath("$.data.title").value("更新的待辦事項"))
                .andExpect(jsonPath("$.data.completed").value(true));
        verify(todoService, times(1)).updateTodo(eq(1L), any(Todo.class));
    }
    @Test
    @WithMockUser(username = "user")
    public void deleteTodo() throws Exception {
        when(todoService.deleteTodo(1L)).thenReturn(true);
        mockMvc.perform(delete("/api/todos/1").with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true));
        verify(todoService, times(1)).deleteTodo(1L);
    }
}
在 setUp 中建立一個測試的使用者
在每個測試方法中,我們使用 REST Assured 提供的 .auth().basic(username, password) 來設定 HTTP 基本認證
這會在每個請求的標頭中添加 Authorization: Basic ... 
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
public class TodoEndToEndTest {
    @LocalServerPort
    private int port;
    @Autowired
    private TodoRepository todoRepository;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    private String username = "testuser";
    private String password = "testpassword";
    @BeforeEach
    void setUp() {
        RestAssured.port = port;
        todoRepository.deleteAll();
        userRepository.deleteAll();
        // 建立測試用戶
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setRoles("USER");
        userRepository.save(user);
    }
    @Test
    void createTodo() {
        Todo newTodo = new Todo(null, "測試待辦事項", false);
        given()
            .auth().basic(username, password)
            .contentType(ContentType.JSON)
            .body(newTodo)
        .when()
            .post("/api/todos")
        .then()
            .statusCode(200)
            .body("success", equalTo(true))
            .body("data.title", equalTo("測試待辦事項"))
            .body("data.completed", equalTo(false));
        assertThat(todoRepository.findAll()).hasSize(1);
    }
    @Test
    void getAllTodos() {
        Todo todo1 = new Todo(null, "測試待辦事項", false);
        Todo todo2 = new Todo(null, "測試待辦事項2", true);
        todoRepository.saveAll(Arrays.asList(todo1, todo2));
        given()
            .auth().basic(username, password)
        .when()
            .get("/api/todos")
        .then()
            .statusCode(200)
            .body("success", equalTo(true))
            .body("data", hasSize(2))
            .body("data[0].title", equalTo("測試待辦事項"))
            .body("data[0].completed", equalTo(false))
            .body("data[1].title", equalTo("測試待辦事項2"))
            .body("data[1].completed", equalTo(true));
    }
    @Test
    void getTodo() {
        Todo savedTodo = todoRepository.save(new Todo(null, "測試待辦事項", false));
        given()
            .auth().basic(username, password)
        .when()
            .get("/api/todos/{id}", savedTodo.getId())
        .then()
            .statusCode(200)
            .body("success", equalTo(true))
            .body("data.title", equalTo("測試待辦事項"))
            .body("data.completed", equalTo(false));
    }
    @Test
    void updateTodo() {
        Todo savedTodo = todoRepository.save(new Todo(null, "原始待辦事項", false));
        Todo updatedTodo = new Todo(savedTodo.getId(), "更新後的待辦事項", true);
        given()
            .auth().basic(username, password)
            .contentType(ContentType.JSON)
            .body(updatedTodo)
        .when()
            .put("/api/todos/{id}", savedTodo.getId())
        .then()
            .statusCode(200)
            .body("success", equalTo(true))
            .body("data.title", equalTo("更新後的待辦事項"))
            .body("data.completed", equalTo(true));
        Todo actualTodo = todoRepository.findById(savedTodo.getId()).orElseThrow();
        assertThat(actualTodo.getTitle()).isEqualTo("更新後的待辦事項");
        assertThat(actualTodo.isCompleted()).isTrue();
    }
    @Test
    void deleteTodo() {
        Todo savedTodo = todoRepository.save(new Todo(null, "要刪除的待辦事項", false));
        given()
            .auth().basic(username, password)
        .when()
            .delete("/api/todos/{id}", savedTodo.getId())
        .then()
            .statusCode(200)
            .body("success", equalTo(true));
        assertThat(todoRepository.findAll()).isEmpty();
    }
}
測試程式碼都修改完後,執行所有的測試,應該都是呈現綠燈

在這篇文章中,我們深入探討了如何調整 Spring Boot 應用程式的測試,以適應加入 Spring Security 後的變化
透過這些調整,我們不僅確保了應用程式的安全性,還保證了測試的完整性和有效性
同步刊登於 Blog 「Spring Boot API 開發:從 0 到 1」Day 34 Spring Security 測試
我的粉絲專頁
圖片來源:AI 產生