🐙

Spring Bootの単体テスト - REST API

2023/12/12に公開

今回は一般的なTODOアプリのREST APIに対する単体テストをする想定とします。* こんなAPI

テストを書くときの瞬発力を上げるためのまとめなので、アノテーションの詳細は深くは書きません。ご了承ください。

環境は、Java 17, Spring Boot 3.x.x, MyBatisを使用しています。

Repository の単体テスト

Repositoryの役割はデータベースアクセスなので、そこをテストします。
私はMyBatisを使用しているので@MyBatisTestアノテーションを記述します。

@MyBatisTest
class TodoMapperTest {

  @Autowired
  TodoMapper todoMapper;

  @Test
  void 指定したIDTODOを取得できること() {
    Todo todo = todoMapper.findById(1);
    assertThat(todo.getTitle()).isEqualTo("Repositoryのテスト書く");
    assertThat(todo.isCompleted()).isEqualTo(true);
  }
}

Repository のテストで重要なのは、テスト用のデータベースのデータの用意です。

テスト用のデータベースの準備に利用するのが @Sqlアノテーションです。
@Sqlアノテーションの引数として、パスを指定すると、テストの実行前に記載されたSQLが実行されます。

また@Transactionalアノテーションはテストにより追加された処理をロールバックするために利用しています。
テスト用に作成されたデータは綺麗にしてからテストを終えないといけないと覚えておきましょう。

上記を踏まえると、先ほどの例では以下のように定義します。

@MyBatisTest
@Sql(
    {
        "classpath:/sql/delete-todo.sql",
        "classpath:/sql/insert-todo.sql"
    }
)
@Transactional
class TodoMapperTest {

  @Autowired
  TodoMapper todoMapper;

  @Test
  void 指定したIDTodoを取得できること() {
    Todo todo = todoMapper.findById(1);
    assertThat(todo.getTitle()).isEqualTo("Repositoryのテスト書く");
    assertThat(todo.isCompleted()).isEqualTo(true);
  }

  @Test
  void 指定したIDTodoを更新できること() {
    TodoRequest req = new TodoRequest();
    req.setTitle("Todoの更新");
    req.setIsCompleted(false);

    todoMapper.updateTodo(1, req);
    assertThat(todoMapper.findById(1).map(Todo::getTitle).orElseThrow()).isEqualTo("Todoの更新");
  }
}

全ての項目をアサーションで確認するのか、要素の1つが一致していれば良しとするのかはプロジェクトによります。

Service の単体テスト

Serviceの役割は業務ロジックなので、期待通り動作するかをテストします。
あくまでもService層の単体テストなので、Repositoryへのアクセスはせずにモックします。

モックする時に利用するのは、Mockitoというライブラリです。SpringBootの単体テストで必須です。

利用するアノテーションは @ExtendedWith@InjectMocks@Mockの3つです。

ざっくり @ExtendedWithは指定したクラスを呼び出します。
MockitoExtensionクラスは@Injectmocks@Mockを使えるようにするために必要なクラスです。

@ExtendedWith(MocitoExtension.class)
class TodoServiceImplTest{
 
  @InjectMocks
  TodoServiceImpl todoService;
  
  @Mock
  TodoMapper todoMapper;
  
  @Test
  void 指定したIDTodoを取得できること(){
    Todo todo = new Todo();
    todo.setTitle("Serviceの単体テスト");
    
    doReturn(todo).when(todoMapper).findById(1);
    Todo actual = todoService.findById(1);
    assertThat(actual.getTitle()).isEqualTo("Serviceの単体テスト");
  }
  
  @Test
  void Todoを作成できること(){
    TodoRequest todoRequest = new TodoRequest();
    todoRequest.setTitle("voidなServiceのテスト");
    todoRequest.setIsCompleted(false);
    
    todoService.create(todoRequest);
    
    verify(todoMapper).create(any());
  }

}

今回はverifyメソッドも抑えておけば完璧なはずです。
verifyメソッドはモックオブジェクトのメソッドが呼ばれたことを確認するメソッドです。
Serviceを実行できていれば、モックされているMapperのメソッドも呼ばれていることを確認します。
戻り値がないメソッドのテストで使用します。

Mockitoのより詳細な使い方は最後に記載している参考書籍で確認してみてください。

Controller の単体テスト

Controllerはリクエストを受け付けてから処理を開始します。
テストのために実際にリクエストを送ることはできないので、今回はリクエストをモックしてテストするとイメージします。

また、Controllerの単体テストなので、Service層もモックします。

Controllerの単体テストでは@WebMvcTest@MockBeanアノテーションを利用します。
さらに、MockMvcオブジェクトについて理解しておけば、Controllerのテストで困ることは減るはずです。

@MockBeanを追加すると、付与されたオブジェクト(今回はService)がモックされます。

@WebMvcTest(TodoController.class)
class TodoControllerTest{
  
  @Autowired 
  MockMvc mockMvc;
  
  @Mockbean
  TodoServiceImpl todoService;
  
  @Test
  void 指定したIDTODOを取得できること() throws Exception{
    mockMvc.perform(get("/todo/{id}", 1))
        .andExpect(status.isOk())
        .andExpect(content().json(
                """
                  {
                    "id": 1,
                    "title": "Controllerの単体テスト",
                    "completed: false
                  }
                """
        ));
    
    verify(todoService).findById(any());
  }
  
  @Test
  void 新しいTodoを登録できること() throws Exception{
    Todo todo = new Todo();
    todo.setId(999);
    doReturn(todo).when(todoService).register(any());
    
    mockMvc.perform(
        post("/todo").contentType(MediaType.APPLICATION_JSON).content(
            """
              {
                "title": "Postのテスト",
                "completed": false
              }
            """))
        .andExpect(status().isCreated());
    
    verify(todoService).register(any());
  }
}

個人的には、Controllerの単体テストが一番難しいです。。。。

個人的にはレスポンスステータス、および、レスポンス内容を確認して、
モックされたサービスのメソッドが呼び出しされていることを確認できていれば十分だと考えています。

まとめ

私はエンジニアになって半年もないですが、学習時にはこんないい書籍なかった。。。
もっと早く出して欲しかったと切実に思うくらい、この書籍には助けられてます。

参考書籍 - プロになるためのSpring入門 ゼロからの開発力養成講座

そして、改めて自分のポートフォリオ見返してみると、ある程度と書籍に出てくるテストはできていた。
反射的にテスト書くときの内容が出てこないというのが課題ですが、単純にコードを書く量の問題かと反省。。。

テストかけないと問題の再現ができず、トラブルシューティングに時間がかかるので
しばらくの間、Spring Bootで行うテストについてひたすらまとめます。

結合テストは、学習したら別途記事にまとめます。

Discussion