【Spring】Mokitoを使って@RestControllerAdviceを使ったエラーハンドラのテストを行う

2020年8月29日

Mockitoを使うとエラーハンドラのテストが作成しやすい

Spring MVCでWeb APIを作成する場合@RestControllerAdviceを使ってエラーハンドラを作成することがあると思います。このエラーハンドラで想定通りにエラー処理が行えているか検証するためにJUnitでテストを作成します。

エラーハンドラで処理する例外にはAPIに対する入力データだけでは発生しない例外も含まれることが多いです。このようなときはMokitoを使用し検証したい例外を投げることでエラーハンドラのテストを簡単に行うことができます。

Mockitoの使い方は以下の記事でも解説していますので参考にしてみて下さい。

この記事での実行環境は以下の通りです。

  • Java11
  • Spring Boot 2.3.1
  • JUnit 5

また、LombokとAssertJを使用しています。

テスト対象のエラーハンドラとエラーレスポンス

Spring MVCの@RestControllerを使用して作成したWeb APIの共通エラーハンドラとして以下のクラスを作成しています。

package com.example.demo.api;

import com.example.demo.exception.EmployeeNotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class ErrorHandlerController {

    @ExceptionHandler({AccessDeniedException.class})
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public ErrorResponse handleException(AccessDeniedException e) {
        log.error("Error:", e.getMessage());
        return new ErrorResponse("notAuthorized", "The request was not authorized.");
    }

    @ExceptionHandler({EmployeeNotFoundException.class})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleEmployeeNotFoundException(EmployeeNotFoundException e) {
        log.error("Error:", e.getMessage());
        return new ErrorResponse("notFound", "The Employee was not found.");
    }

    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleException(Exception e) {
        log.error("Error:", e.getMessage());
        return new ErrorResponse("systemError", "System error occurred.");
    }
}

エラーハンドラからは以下の形式のエラーレスポンスを返すようにします。

package com.example.demo.api;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {

    private String errorCode;
    private String message;
}

コントローラの例

エラーハンドラのテストで使用するコントローラを以下のように作成しています。

基本的なレイヤードアーキテクチャにしたがって作成しており、このコントローラのemployeeRepositoryはDIコンテナによりインジェクションされるように作っています。Lombokの@RequiredArgsConstructorを使うことでコンストラクタインジェクションに必要なコード量を減らしています。

この記事はエラーハンドラのテスト方法がメインですので、簡易的にコントローラからリポジトリを呼び出す形にしています。

employeeRepositoryをMockitoでモック化しemployeeRepository.findById(id)実行時に目的の例外を投げさせることでエラーハンドラのテストを行います。

package com.example.demo.api;

import com.example.demo.domain.repository.EmployeeRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class EmployeeController {

    private final EmployeeRepository employeeRepository;

    @PreAuthorize("hasAuthority('GetEmployee')")
    @GetMapping("employees/{id}")
    public EmployeeResponse show(@PathVariable("id") int id) {

        var employee = employeeRepository.findById(id);
        var employeeResponse = new EmployeeResponse(employee.getId(), employee.getName(), employee.getAge());
       return employeeResponse;
    }
}

エラーハンドラのテストコード例

テストコードは以下のように作成しています。

まず、Mokitoを使ってEmployeeControllerへはモックをインジェクションするようにします。

コントローラへのHTTPリクエストを投げる処理は通常のコントローラのテストと同様にMockMvcを使用しますが、EmployeeControllerをMokitoを使ってインスタンス化しているため、MockMvcBuilders.standaloneSetupメソッドを使用してMockMvcのインスタンス化を行います。

そして、setControllerAdviceメソッドを使いテスト対象のErrorHandlerControllerのインスタンスをControllerAdviceに設定します。

あとはMockitoでemployeeRepository.findByIdメソッドを実行したときに例外を投げるようにモック化しています。

package com.example.demo.api;

import com.example.demo.domain.repository.EmployeeRepository;
import com.example.demo.exception.EmployeeNotFoundException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(MockitoExtension.class)
public class ErrorHandlerControllerTest {

    @InjectMocks
    private EmployeeController employeeController;

    @Mock
    private EmployeeRepository employeeRepository;

    private MockMvc mockMvc;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @BeforeEach
    void init() {

        mockMvc = MockMvcBuilders.standaloneSetup(employeeController)
                .setControllerAdvice(new ErrorHandlerController())
                .build();
    }

    @Test
    public void handleEmployeeNotFoundExceptionTest() throws Exception {

        // findByIdメソッドの実行時にEmployeeNotFoundExceptionを投げる
        when(employeeRepository.findById(anyInt())).thenThrow(new EmployeeNotFoundException(""));

        var responseString = mockMvc.perform(get("/employees/1"))
                .andExpect(status().isNotFound())
                .andReturn().getResponse().getContentAsString();

        var errorResponse = objectMapper.readValue(responseString, ErrorResponse.class);

        assertThat(errorResponse.getErrorCode()).isEqualTo("notFound");
        assertThat(errorResponse.getMessage()).isEqualTo("The Employee was not found.");

    }
}

このようにMokitoを使うことで入力データでは発生させにくい例外も簡単にテストすることができます。