Skip to content

バリデーションエラーのハンドリング ​

REST APIのバリデーションエラーをハンドリングし、curlでの打鍵および単体テストでの異常系チェックが行えるようにします。

実装 ​

pom.xml ​

xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

controller/AuthController.java ​

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 org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
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;

/**
 * 認証コントローラーです。
 *
 * @author nob
 */
@RestController
@RequestMapping(value = "/api/v1")
@RequiredArgsConstructor
public class AuthController {

    @NonNull
    private final AuthService authService;

    /**
     * 認証処理を呼び出します。
     *
     * @param request 認証リクエスト
     * @return 認証結果
     */
    @PostMapping(value = "/login")
    LoginResponse login(@RequestBody @Valid LoginRequest request) { // @Validが付いているinModelについて検証を行います。

        return new LoginResponse(authService.login(new LoginInModel(request.name(), request.password())).valid());
    }
}

controller/model/LoginRequest.java ​

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 ​

shell
# エラーメッセージの辞書ファイルです。

loginRequest.name = ユーザ名
loginRequest.name.NotBlank = {loginRequest.name}を入力してください。

handler/MethodArgumentNotValidExceptionHandler.java ​

java
package nob.example.easyapp.handler;

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 = e.getAllErrors().stream().map(err -> err.getDefaultMessage()).toList();

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(new MethodArgumentNotValidExceptionResponse(messageList));
    }

    /**
     * バリデーションエラー発生時のレスポンスボディです。
     *
     * @param messageList エラーメッセージのリスト
     */
    public record MethodArgumentNotValidExceptionResponse(List<String> messageList) {
    }
}

テスト ​

AuthControllerTest.java ​

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.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;

/**
 * AuthControllerのテストクラスです。
 *
 * @author nob
 */
@WebMvcTest(AuthController.class)
public class AuthControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockitoBean
    private AuthService authService;

    /**
     * loginのテスト ユーザ名がnullでエラー
     */
    @Test
    void testLoginNullName() {

        // リクエストの作成
        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
{"messageList":["ユーザ名を入力してください。"]}