国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - Java教程 - SpringBoot 使用@WebMvcTest測試MVC Web Controller

SpringBoot 使用@WebMvcTest測試MVC Web Controller

2022-03-10 00:51橫掃bug Java教程

這篇文章主要介紹了SpringBoot 使用@WebMvcTest測試MVC Web Controller,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

在有關使用 Spring Boot 進行測試的系列的第二部分中,我們將了解 Web 控制器。首先,我們將探索 Web 控制器的實際作用,這樣我們就可以構建涵蓋其所有職責的測試。

然后,我們將找出如何在測試中涵蓋這些職責。只有涵蓋了這些職責,我們才能確保我們的控制器在生產環境中按預期運行。

依賴

我們將使用 JUnit Jupiter (JUnit 5) 作為測試框架,使用 Mockito 進行模擬,使用 AssertJ 來創建斷言,使用 Lombok 來減少樣板代碼:

?
1
2
3
4
5
6
7
dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
    testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

AssertJ 和 Mockito 跟隨 spring-boot-starter-test 依賴自動獲得。

Web 控制器的職責

讓我們從一個典型的 REST 控制器開始:

?
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequiredArgsConstructor
class RegisterRestController {
    private final RegisterUseCase registerUseCase;
    @PostMapping("/forums/{forumId}/register")
    UserResource register(@PathVariable("forumId") Long forumId, @Valid @RequestBody UserResource userResource,
            @RequestParam("sendWelcomeMail") boolean sendWelcomeMail) {
        User user = new User(userResource.getName(), userResource.getEmail());
        Long userId = registerUseCase.registerUser(user, sendWelcomeMail);
        return new UserResource(userId, user.getName(), user.getEmail());
    }
}

控制器方法用 @PostMapping 注解來定義它應該偵聽的 URL、HTTP 方法和內容類型。

它通過用 @PathVariable、@RequestBody 和 @RequestParam 注解的參數獲取輸入,這些參數會從傳入的 HTTP 請求中自動填充。

參數可以使用 @Valid 進行注解,以指示 Spring 應該對它們 bean 驗證。

然后控制器使用這些參數,調用業務邏輯返回一個普通的 Java 對象,默認情況下該對象會自動映射到 JSON 并寫入 HTTP 響應體。

這里有很多 spring 魔法。總之,對于每個請求,控制器通常會執行以下步驟:

SpringBoot 使用@WebMvcTest測試MVC Web Controller

控制器顯然有很多工作要做!

我們應該注意不要添加更多的職責,比如執行業務邏輯。否則,我們的控制器測試將變得臃腫且無法維護。

我們將如何編寫有意義的測試,涵蓋所有這些職責?

單元測試還是集成測試?

我們寫單元測試嗎?還是集成測試?到底有什么區別?讓我們討論這兩種方法并決定其中一種。

在單元測試中,我們將單獨測試控制器。這意味著我們將實例化一個控制器對象,模擬業務邏輯,然后調用控制器的方法并驗證響應。

這對我們有用嗎?讓我們檢查一下可以單獨的單元測試中涵蓋上面確定的 6 個職責中的哪一個:

SpringBoot 使用@WebMvcTest測試MVC Web Controller

與 Spring 的集成測試會啟動一個包含我們需要的所有 bean 的 Spring 應用程序上下文。這包括負責偵聽某些 URL、與 JSON 之間進行序列化和反序列化以及將異常轉換為 HTTP 的框架 bean。這些 bean 將評估簡單單元測試會忽略的注釋。總之,簡單的單元測試不會覆蓋 HTTP 層。所以,我們需要在我們的測試中引入 Spring 來為我們做 HTTP 魔法。因此,我們正在構建一個集成測試來測試我們的控制器代碼和 Spring 為 HTTP 支持提供的組件之間的集成。

那么,我們該怎么做呢?

使用 @WebMvcTest 驗證控制器職責

Spring Boot 提供了 @WebMvcTest 注釋來啟動一個應用程序上下文,該上下文只包含測試 Web 控制器所需的 bean:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = RegisterRestController.class)
class RegisterRestControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private ObjectMapper objectMapper;
    @MockBean
    private RegisterUseCase registerUseCase;
  @Test
  void whenValidInput_thenReturns200() throws Exception {
    mockMvc.perform(...);
  }
}

@ExtendWith

本教程中的代碼示例使用 @ExtendWith 批注告訴 JUnit 5 啟用 Spring 支持。從 Spring Boot 2.1 開始,我們不再需要加載 SpringExtension,因為它作為元注釋包含在 Spring Boot 測試注解中,例如 @DataJpaTest、@WebMvcTest 和 @SpringBootTest。

我們現在可以 @Autowire 從應用程序上下文中獲取我們需要的所有 bean。Spring Boot 自動提供了像 ObjectMapper 這樣的 bean 來映射到 JSON 和一個 MockMvc 實例來模擬 HTTP 請求。

我們使用 @MockBean 來模擬業務邏輯,因為我們不想測試控制器和業務邏輯之間的集成,而是控制器和 HTTP 層之間的集成。@MockBean 自動用 Mockito 模擬替換應用程序上下文中相同類型的 bean。

您可以在我關于模擬的文章中閱讀有關 @MockBean 注解的更多信息。

使用帶或不帶 controllers 參數的 @WebMvcTest?

通過在上面的示例中將 controllers 參數設置為 RegisterRestController.class,我們告訴 Spring Boot 將為此測試創建的應用程序上下文限制為給定的控制器 bean 和 Spring Web MVC 所需的一些框架 bean。我們可能需要的所有其他 bean 必須單獨包含或使用 @MockBean 模擬。

如果我們不使用 controllers 參數,Spring Boot 將在應用程序上下文中包含所有控制器。因此,我們需要包含或模擬掉任何控制器所依賴的所有 bean。這使得測試設置更加復雜,具有更多的依賴項,但節省了運行時間,因為所有控制器測試都將重用相同的應用程序上下文。

我傾向于將控制器測試限制在最窄的應用程序上下文中,以使測試獨立于我在測試中甚至不需要的 bean,即使 Spring Boot 必須為每個單獨的測試創建一個新的應用程序上下文。

讓我們來回顧一下每個職責,看看我們如何使用 MockMvc 來驗證每一個職責,以便構建我們力所能及的最好的集成測試。

1.驗證 HTTP 請求匹配

驗證控制器是否偵聽某個 HTTP 請求非常簡單。我們只需調用 MockMvc 的 perform() 方法并提供我們要測試的 URL:

?
1
2
3
mockMvc.perform(post("/forums/42/register")
    .contentType("application/json"))
    .andExpect(status().isOk());

除了驗證控制器對特定 URL 的響應之外,此測試還驗證正確的 HTTP 方法(在我們的示例中為 POST)和正確的請求內容類型。我們上面看到的控制器會拒絕任何具有不同 HTTP 方法或內容類型的請求。

請注意,此測試仍然會失敗,因為我們的控制器需要一些輸入參數。

更多匹配 HTTP 請求的選項可以在 MockHttpServletRequestBuilder 的 Javadoc 中找到。

2.驗證輸入序列化

為了驗證輸入是否成功序列化為 Java 對象,我們必須在測試請求中提供它。輸入可以是請求正文的 JSON 內容 (@RequestBody)、URL 路徑中的變量 (@PathVariable) 或 HTTP 請求參數 (@RequestParam):

?
1
2
3
4
5
6
7
8
9
10
@Test
void whenValidInput_thenReturns200() throws Exception {
  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
 
   mockMvc.perform(post("/forums/{forumId}/register", 42L)
        .contentType("application/json")
        .param("sendWelcomeMail", "true")
        .content(objectMapper.writeValueAsString(user)))
        .andExpect(status().isOk());
}

我們現在提供路徑變量 forumId、請求參數 sendWelcomeMail 和控制器期望的請求正文。請求正文是使用 Spring Boot 提供的 ObjectMapper 生成的,將 UserResource 對象序列化為 JSON 字符串。

如果測試結果為綠色,我們現在知道控制器的 register() 方法已將這些參數作為 Java 對象接收,并且它們已從 HTTP 請求中成功解析。

3.驗證輸入驗證

假設 UserResource 使用 @NotNull 注釋來拒絕 null 值:

?
1
2
3
4
5
6
7
@Value
public class UserResource {
    @NotNull
    private final String name;
    @NotNull
    private final String email;
}

當我們將 @Valid 注解添加到方法參數時,Bean 驗證會自動觸發,就像我們在控制器中使用 userResource 參數所做的那樣。因此,對于快樂路徑(即驗證成功時),我們在上一節中創建的測試就足夠了。

如果我們想測試驗證是否按預期失敗,我們需要添加一個測試用例,在該用例中我們將無效的 UserResource JSON 對象發送到控制器。然后我們期望控制器返回 HTTP 狀態 400(錯誤請求):

?
1
2
3
4
5
6
7
8
9
@Test
void whenNullValue_thenReturns400() throws Exception {
  UserResource user = new UserResource(null, "zaphod@galaxy.net");
 
  mockMvc.perform(post("/forums/{forumId}/register", 42L)
      ...
      .content(objectMapper.writeValueAsString(user)))
      .andExpect(status().isBadRequest());
}

根據驗證對應用程序的重要性,我們可能會為每個可能的無效值添加這樣的測試用例。但是,這會很快增加很多測試用例,因此您應該與您的團隊討論您希望如何處理項目中的驗證測試。

4.驗證業務邏輯調用

接下來,我們要驗證業務邏輯是否按預期調用。在我們的例子中,業務邏輯由 RegisterUseCase 接口提供,并需要一個 User 對象和一個 boolean 值作為輸入:

?
1
2
3
interface RegisterUseCase {
    Long registerUser(User user, boolean sendWelcomeMail);
}

我們希望控制器將傳入的 UserResource 對象轉換為 User 并將此對象傳遞給 registerUser() 方法。

為了驗證這一點,我們可以要求 RegisterUseCase 模擬,它已使用 @MockBean 注解注入到應用程序上下文中:

?
1
2
3
4
5
6
7
8
9
@Test
void whenValidInput_thenMapsToBusinessModel() throws Exception {
  UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
  mockMvc.perform(...);
  ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
  verify(registerUseCase, times(1)).registerUser(userCaptor.capture(), eq(true));
  assertThat(userCaptor.getValue().getName()).isEqualTo("Zaphod");
  assertThat(userCaptor.getValue().getEmail()).isEqualTo("zaphod@galaxy.net");
}

在執行了對控制器的調用之后,我們使用 ArgumentCaptor 來捕獲傳遞給 RegisterUseCase.registerUser() 的 User 對象并斷言它包含預期值。

調用 verify 檢查 registerUser() 是否被調用過一次。

請注意,如果我們對 User 對象進行大量斷言,我們可以 創建自己的自定義 Mockito 斷言方法 以獲得更好的可讀性。

5.驗證輸出序列化

調用業務邏輯后,我們希望控制器將結果映射到 JSON 字符串并將其包含在 HTTP 響應中。在我們的例子中,我們希望 HTTP 響應正文包含一個有效的 JSON 格式的 UserResource 對象:

?
1
2
3
4
5
6
7
8
9
10
11
@Test
void whenValidInput_thenReturnsUserResource() throws Exception {
  MvcResult mvcResult = mockMvc.perform(...)
      ...
      .andReturn();
  UserResource expectedResponseBody = ...;
  String actualResponseBody = mvcResult.getResponse().getContentAsString();
 
  assertThat(actualResponseBody).isEqualToIgnoringWhitespace(
              objectMapper.writeValueAsString(expectedResponseBody));
}

要對響應主體進行斷言,我們需要使用 andReturn() 方法將 HTTP 交互的結果存儲在 MvcResult 類型的變量中。

然后我們可以從響應正文中讀取 JSON 字符串,并使用 isEqualToIgnoringWhitespace() 將其與預期的字符串進行比較。我們可以使用 Spring Boot 提供的 ObjectMapper 從 Java 對象構建預期的 JSON 字符串。

請注意,我們可以通過使用自定義的 ResultMatcher 使其更具可讀性,稍后對此加以描述。

6.驗證異常處理

通常,如果發生異常,控制器應該返回某個 HTTP 狀態。400 --- 如果請求有問題,500 --- 如果出現異常,等等。

默認情況下,Spring 會處理大多數這些情況。但是,如果我們有自定義異常處理,我們想測試它。假設我們想要返回一個結構化的 JSON 錯誤響應,其中包含請求中每個無效字段的字段名稱和錯誤消息。我們會像這樣創建一個 @ControllerAdvice:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ControllerAdvice
class ControllerExceptionHandler {
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        ErrorResult errorResult = new ErrorResult();
        for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
            errorResult.getFieldErrors()
                    .add(new FieldValidationError(fieldError.getField(), fieldError.getDefaultMessage()));
        }
        return errorResult;
    }
    @Getter
    @NoArgsConstructor
    static class ErrorResult {
        private final List<FieldValidationError> fieldErrors = new ArrayList<>();
        ErrorResult(String field, String message) {
            this.fieldErrors.add(new FieldValidationError(field, message));
        }
    }
    @Getter
    @AllArgsConstructor
    static class FieldValidationError {
        private String field;
        private String message;
    }
}

如果 bean 驗證失敗,Spring 將拋出 MethodArgumentNotValidException。我們通過將 Spring 的 FieldError 對象映射到我們自己的 ErrorResult 數據結構來處理這個異常。在這種情況下,異常處理程序會導致所有控制器返回 HTTP 狀態 400,并將 ErrorResult 對象作為 JSON 字符串放入響應正文中。

為了驗證這確實發生了,我們擴展了我們之前對失敗驗證的測試:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
void whenNullValue_thenReturns400AndErrorResult() throws Exception {
  UserResource user = new UserResource(null, "zaphod@galaxy.net");
  MvcResult mvcResult = mockMvc.perform(...)
          .contentType("application/json")
          .param("sendWelcomeMail", "true")
          .content(objectMapper.writeValueAsString(user)))
          .andExpect(status().isBadRequest())
          .andReturn();
  ErrorResult expectedErrorResponse = new ErrorResult("name", "must not be null");
  String actualResponseBody =
      mvcResult.getResponse().getContentAsString();
  String expectedResponseBody =
      objectMapper.writeValueAsString(expectedErrorResponse);
  assertThat(actualResponseBody)
      .isEqualToIgnoringWhitespace(expectedResponseBody);
}

同樣,我們從響應正文中讀取 JSON 字符串,并將其與預期的 JSON 字符串進行比較。此外,我們檢查響應狀態是否為 400。

這也可以以可讀性更強的方式實現,我們接下來將要學習。

創建自定義 ResultMatcher

某些斷言很難寫,更重要的是,很難閱讀。特別是當我們想要將來自 HTTP 響應的 JSON 字符串與預期值進行比較時,它需要大量代碼,正如我們在最后兩個示例中看到的那樣。

幸運的是,我們可以創建自定義的 ResultMatcher,我們可以在 MockMvc 的流暢 API 中使用它們。讓我們看看如何做到這一點。

匹配 JSON 輸出

使用以下代碼來驗證 HTTP 響應正文是否包含某個 Java 對象的 JSON 表示不是很好嗎?

?
1
2
3
4
5
6
7
8
@Test
void whenValidInput_thenReturnsUserResource_withFluentApi() throws Exception {
  UserResource user = ...;
  UserResource expected = ...;
  mockMvc.perform(...)
      ...
      .andExpect(responseBody().containsObjectAsJson(expected, UserResource.class));
}

不再需要手動比較 JSON 字符串。它的可讀性要好得多。事實上,代碼是如此的一目了然,這里我無需解釋。

為了能夠使用上面的代碼,我們創建了一個自定義的 ResultMatcher:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ResponseBodyMatchers {
    private ObjectMapper objectMapper = new ObjectMapper();
    public <T> ResultMatcher containsObjectAsJson(Object expectedObject, Class<T> targetClass) {
        return mvcResult -> {
            String json = mvcResult.getResponse().getContentAsString();
            T actualObject = objectMapper.readValue(json, targetClass);
            assertThat(actualObject).isEqualToComparingFieldByField(expectedObject);
        };
    }
    static ResponseBodyMatchers responseBody() {
        return new ResponseBodyMatchers();
    }
}

靜態方法 responseBody() 用作我們流暢的 API 的入口點。它返回實際的 ResultMatcher,它從 HTTP 響應正文解析 JSON,并將其與傳入的預期對象逐個字段進行比較。

匹配預期的驗證錯誤

我們甚至可以更進一步簡化我們的異常處理測試。我們用了 4 行代碼來驗證 JSON 響應是否包含某個錯誤消息。我們可以改為一行:

?
1
2
3
4
5
6
7
8
9
@Test
void whenNullValue_thenReturns400AndErrorResult_withFluentApi() throws Exception {
  UserResource user = new UserResource(null, "zaphod@galaxy.net");
  mockMvc.perform(...)
      ...
      .content(objectMapper.writeValueAsString(user)))
      .andExpect(status().isBadRequest())
      .andExpect(responseBody().containsError("name", "must not be null"));
}

同樣,代碼是自解釋的。

為了啟用這個流暢的 API,我們必須從上面添加方法 containsErrorMessageForField() 到我們的 ResponseBodyMatchers 類:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ResponseBodyMatchers {
    private ObjectMapper objectMapper = new ObjectMapper();
    public ResultMatcher containsError(String expectedFieldName, String expectedMessage) {
        return mvcResult -> {
            String json = mvcResult.getResponse().getContentAsString();
            ErrorResult errorResult = objectMapper.readValue(json, ErrorResult.class);
            List<FieldValidationError> fieldErrors = errorResult.getFieldErrors().stream()
                    .filter(fieldError -> fieldError.getField().equals(expectedFieldName))
                    .filter(fieldError -> fieldError.getMessage().equals(expectedMessage)).collect(Collectors.toList());
            assertThat(fieldErrors).hasSize(1).withFailMessage(
                    "expecting exactly 1 error message" + "with field name '%s' and message '%s'", expectedFieldName,
                    expectedMessage);
        };
    }
    static ResponseBodyMatchers responseBody() {
        return new ResponseBodyMatchers();
    }
}

所有丑陋的代碼都隱藏在這個輔助類中,我們可以在集成測試中愉快地編寫干凈的斷言。

結論

Web 控制器有很多職責。如果我們想用有意義的測試覆蓋一個 web 控制器,僅僅檢查它是否返回正確的 HTTP 狀態是不夠的。

通過 @WebMvcTest,Spring Boot 提供了我們構建 Web 控制器測試所需的一切,但為了使測試有意義,我們需要記住涵蓋所有職責。否則,我們可能會在運行時遇到丑陋的驚喜。

以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。

原文鏈接:https://blog.csdn.net/m0_60720471/article/details/121064168

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 黄a在线观看 | 久草视频免费看 | 欧美国产综合 | 九九精品在线 | 国产精品第一 | 欧美成人精品一区二区 | 亚洲国产精品视频 | 色偷偷888欧美精品久久久 | 在线欧美亚洲 | 欧美精三区欧美精三区 | 成人午夜在线播放 | av中文字幕第一页 | 依人在线 | 亚洲一区二区三区在线播放 | 欧美日韩视频一区二区 | 色淫av| 欧美自拍小视频 | 性激烈欧美三级在线播放狩猎 | 国产精品久久久久久亚洲调教 | 亚洲色视频 | 国产午夜精品一区二区三区嫩草 | 欧美大片在线观看 | 日韩欧美视频 | 欧美一级在线观看 | 成人亚洲网 | 国产精品一区二 | 国产亚洲精品美女久久久久久久久久 | 国产一区二区三区视频在线观看 | av在线播放不卡 | 免费a级毛片在线观看 | 国产成人一区二区啪在线观看 | 成人欧美一区二区三区在线观看 | 精品视频一区二区三区 | 亚洲视频1区 | 狠狠草视频 | 欧美日韩中文字幕 | www成人精品 | 亚洲在线播放 | 中文字幕一区二区三区四区 | 欧美成人精品一区二区三区 | 亚洲第一黄色网 |