Skip to content

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

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":["ユーザ名を入力してください。"]}