iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
1
Modern Web

站在Web前端人員角度,學習 Spring Boot 後端開發系列 第 19

Day 19 - Spring Boot HTTP的請求也可以模擬測試?使用MockMvc

  • 分享至 

  • xImage
  •  

「別試著理解它,去感受它。」 -《TENET天能》

經過上篇測試Service層後,今天要來測試Controller層,由於Controller是透過用戶端使用HTTP呼叫的,所以我們要模擬網路的形式,使用MockMvc 來實作Controller層的測試。

透過輸入URL對Controller進行測試,如果需要啟動伺服器,建立http client進行測試,這樣會使得測試變得很麻煩,比如,啟動速度慢,測試驗證不方便,依賴網路環境等。

MockMvc這個spring framework 實現了對Http請求的模擬端點測試,能夠直接使用網路的形式,轉換到Controller的呼叫,不依賴網路環境,提供了一套驗證的工具,這樣可以使得請求的驗證統一且方便。

https://ithelp.ithome.com.tw/upload/images/20200928/20118857kScY8OjXgQ.png

設置依賴環境

pom.xml Spring Boot 預設建立專案時就會引入spring-boot-starter-test

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

測試Controller

首先要建立一個測試檔案

Spring Boot專案撰寫單元測試要先在類別前加入@SpringBootTest 測試標註。

並用@AutoConfigureMockMvc 啟動時自動注入MockMvc

@MockBean 創造一個假的Service對象。

@SpringBootTest
@AutoConfigureMockMvc
public class TestTodoController {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockBean
    TodoService todoService;
}

介紹一下MockMvc的屬性

  • mockMvc.perform 執行一個請求,並對應到controller。
  • mockMvc.andExpect 期待並驗證回應是否正確。
  • mockMvc.andReturn 最後回應的值(body),可以再利用這個值,做其他Assert驗證

開始單元測試Controller的端點

[GET] /api/todos 端點

@RestController
@RequestMapping("/api")
public class TodoController {
    @Autowired
    TodoService todoService;

    @GetMapping("/todos")
    public ResponseEntity getTodos() {
        Iterable<Todo> todoList = todoService.getTodos();
        return ResponseEntity.status(HttpStatus.OK).body(todoList);
    }
}

測試[GET] /api/todos

@Test
public void testGetTodos() throws Exception {
    // 設定資料
    List<Todo> expectedList = new ArrayList();
    Todo todo = new Todo();
    todo.setTask("洗衣服");
    todo.setId(1);
    expectedList.add(todo);

    // 模擬todoService.getTodos() 回傳 expectedList
    Mockito.when(todoService.getTodos()).thenReturn(expectedList);

    // 模擬呼叫[GET] /api/todos
    String returnString = mockMvc.perform(MockMvcRequestBuilders.get("/api/todos")
            .accept(MediaType.APPLICATION_JSON ))
            .andExpect(status().isOk())
            .andReturn().getResponse().getContentAsString();

    Iterable<Todo> actualList = objectMapper.readValue(returnString, new TypeReference<Iterable<Todo>>() {
    });

    // 判定回傳的body是否跟預期的一樣
    assertEquals(expectedList, actualList);
}

[POST] /api/todos 端點

@RestController
@RequestMapping("/api")
public class TodoController {
    @Autowired
    TodoService todoService;

		@PostMapping("/todos")
	  public ResponseEntity createTodo(@RequestBody Todo todo) {
        Integer rlt = todoService.createTodo(todo);
        return ResponseEntity.status(HttpStatus.CREATED).body(rlt);
    }
}

測試[POST] /api/todos

@Test
public void testrCreateTodo() throws Exception {
    // 設定資料
    Todo mockTodo = new Todo();
    mockTodo.setId(1);
    mockTodo.setTask("洗衣服");
    mockTodo.setStatus(1);

    JSONObject todoObject = new JSONObject();
    todoObject.put("id", 1);
    todoObject.put("task", "洗衣服");

    // 模擬todoService.createTodo(todo) 回傳 id 1
    Mockito.when(todoService.createTodo(mockTodo)).thenReturn(1);

    // 模擬呼叫[POST] /api/todos
    String actual = mockMvc.perform(MockMvcRequestBuilders.post("/api/todos")
            .accept(MediaType.APPLICATION_JSON) //response 設定型別
            .contentType(MediaType.APPLICATION_JSON) // request 設定型別
            .content(String.valueOf(todoObject))) // body 內容
            .andExpect(status().isCreated()) // 預期回應的status code 為 201(Created)
            .andReturn().getResponse().getContentAsString();

}

原來的[DELTE] /api/todos/{id}

@RestController
@RequestMapping("/api")
public class TodoController {
    @Autowired
    TodoService todoService;

		@DeleteMapping("/todos/{id}")
    public ResponseEntity deleteTodo(@PathVariable Integer id) {
        Boolean rlt = todoService.deleteTodo(id);
        if (!rlt) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Id 不存在");
        }
        return ResponseEntity.status(HttpStatus.NO_CONTENT).body("");
    }
}

測試[DELETE] /api/todos

  1. Delete todo 成功
@Test
public void testDeleteTodoSuccess() throws Exception {
    // 模擬todoService.deleteTodo(1) 成功回傳true
    Mockito.when(todoService.deleteTodo(1)).thenReturn(true);

    // 模擬呼叫[DELETE] /api/todos/{id}
    mockMvc.perform(MockMvcRequestBuilders.delete("/api/todos/1")
            .accept(MediaType.APPLICATION_JSON_UTF8 ) //response 設定型別
            .contentType(MediaType.APPLICATION_JSON)) // request 設定型別
            .andExpect(status().isNoContent()); // 預期回應的status code 應為 204(No Content)
}
  1. Delete todo 不存在的id
@Test
public void testDeleteTodoIdNotExist() throws Exception {
      //模擬delete id 不存在,所以todoService.deleteTodo(100)回傳false
      Mockito.when(todoService.deleteTodo(100)).thenReturn(false);

      // 模擬呼叫[DELETE] /api/todos/{id}
      mockMvc.perform(MockMvcRequestBuilders.delete("/api/todos/100")
              .accept(MediaType.APPLICATION_JSON_UTF8 ) //response 設定型別
              .contentType(MediaType.APPLICATION_JSON)) // request 設定型別
              .andExpect(status().isBadRequest()); // 預期回應的status code 為 400(Bad Request)
  }

Reference

Testing the Web Layer

MockMvc 文件


呼~總算寫完測試了,但[PUT]/api/todos/{id}出了點問題,我還在debug中,之後成功抓到蟲後,會再更新文章


上一篇
Day 18 - Spring Boot 單元測試 Service層使用Mockito
下一篇
Day 20 - Spring Boot 組起來再測一遍-整合測試
系列文
站在Web前端人員角度,學習 Spring Boot 後端開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
PollyPO技術
iT邦新手 4 級 ‧ 2020-09-29 09:19:31

Wow~~感覺你越來越厲害了ㄋㄟ!!
(崇拜)

我要留言

立即登入留言