バリデーションエラーのハンドリング
REST APIのバリデーションエラーをハンドリングし、curlでの打鍵および単体テストでの異常系チェックが行えるようにします。
実装
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
controller/AuthController.java
package nob.example.easyapp.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import nob.example.easyapp.controller.model.LoginRequest;
import nob.example.easyapp.controller.model.LoginResponse;
/**
* 認証コントローラーのインターフェースです。
*
* @author nob
*/
@RequestMapping(value = "/api/v1")
public interface AuthController {
/**
* 認証処理を呼び出します。
*
* @param request 認証リクエスト
* @return 認証結果
*/
@PostMapping(value = "/login")
LoginResponse login(@RequestBody @Valid LoginRequest request); // @Validが付いているinModelについて検証を行います。
}
controller/impl/AuthControllerImpl.java
package nob.example.easyapp.controller.impl;
import org.springframework.web.bind.annotation.RestController;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import nob.example.easyapp.controller.AuthController;
import nob.example.easyapp.controller.model.LoginRequest;
import nob.example.easyapp.controller.model.LoginResponse;
import nob.example.easyapp.service.AuthService;
import nob.example.easyapp.service.model.LoginInModel;
/**
* AuthControllerの実装クラスです。
*
* @author nob
*/
@RestController
@RequiredArgsConstructor
public class AuthControllerImpl implements AuthController {
@NonNull
private final AuthService authService;
@Override
public LoginResponse login(LoginRequest request) {
return new LoginResponse(authService.login(new LoginInModel(request.name(), request.password())).valid());
}
}
controller/model/LoginRequest.java
package nob.example.easyapp.controller.model;
import jakarta.validation.constraints.NotBlank;
/**
* 認証向けのリクエストモデルです。
*
* @param name ユーザ名 nullを許可しない
* @param password パスワード
*
* @author nob
*/
public record LoginRequest(@NotBlank(message = "{loginRequest.name.NotBlank}") String name, String password) {
}
resources/ValidationMessages.properties
# エラーメッセージの辞書ファイルです。
loginRequest.name = ユーザ名
loginRequest.name.NotBlank = {loginRequest.name}を入力してください。
handler/MethodArgumentNotValidExceptionHandler.java
package nob.example.easyapp.handler;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* バリデーションによる例外のハンドラです。
*
*/
@RestControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
/**
* バリデーションによる例外が投げられた際に呼ばれるメソッドです。
*
* @param e
* @return エラーメッセージ格納レスポンスボディ
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<MethodArgumentNotValidExceptionResponse> validatorExceptionHandle(
MethodArgumentNotValidException e) {
// エラーメッセージをリストに詰め込む
List<String> messageList = new ArrayList<String>();
for (int i = 0; i < e.getAllErrors().size(); i++) {
messageList.add(e.getAllErrors().get(i).getDefaultMessage());
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new MethodArgumentNotValidExceptionResponse(messageList));
}
/**
* バリデーションエラー発生時のレスポンスボディです。
*
* @param messageList エラーメッセージのリスト
*/
public record MethodArgumentNotValidExceptionResponse(List<String> messageList) {
}
}
テスト
AuthControllerImplTest.java
package nob.example.easyapp.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import nob.example.easyapp.controller.impl.AuthControllerImpl;
import nob.example.easyapp.controller.model.LoginRequest;
import nob.example.easyapp.handler.MethodArgumentNotValidExceptionHandler;
import nob.example.easyapp.service.AuthService;
import nob.example.easyapp.service.model.LoginInModel;
import nob.example.easyapp.service.model.LoginOutModel;
import tools.jackson.databind.ObjectMapper;
/**
* AuthControllerImplのテストクラスです。
*
* @author nob
*/
@WebMvcTest(AuthControllerImpl.class)
public class AuthControllerImplTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockitoBean
private AuthService authService;
/**
* loginのテスト ユーザ名がnullでエラー
*/
@Test
void test_login_nullName() {
// リクエストの作成
LoginRequest request = new LoginRequest(null, "passwd");
// serviceのモック化
Mockito.when(authService.login(new LoginInModel(request.name(), request.password())))
.thenReturn(new LoginOutModel(true));
try {
// API呼び出し
MvcResult result = mockMvc.perform(post("/api/v1/login")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest())
.andReturn();
// 結果のassert
assertThat(result.getResponse().getContentAsString())
.isEqualTo(objectMapper.writeValueAsString(
new MethodArgumentNotValidExceptionHandler.MethodArgumentNotValidExceptionResponse(
List.of("ユーザ名を入力してください。"))));
} catch (Exception e) {
e.printStackTrace();
fail();
}
}
}
API動確
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "", "password": "passwd"}' localhost:8080/api/v1/login
{"errorList":["ユーザ名を入力してください。"]}