Spring Bootの単体テスト - REST API
今回は一般的なTODOアプリのREST APIに対する単体テストをする想定とします。* こんなAPI
テストを書くときの瞬発力を上げるためのまとめなので、アノテーションの詳細は深くは書きません。ご了承ください。
環境は、Java 17, Spring Boot 3.x.x, MyBatisを使用しています。
Repository の単体テスト
Repositoryの役割はデータベースアクセスなので、そこをテストします。
私はMyBatisを使用しているので@MyBatisTest
アノテーションを記述します。
@MyBatisTest
class TodoMapperTest {
@Autowired
TodoMapper todoMapper;
@Test
void 指定したIDのTODOを取得できること() {
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 指定したIDのTodoを取得できること() {
Todo todo = todoMapper.findById(1);
assertThat(todo.getTitle()).isEqualTo("Repositoryのテスト書く");
assertThat(todo.isCompleted()).isEqualTo(true);
}
@Test
void 指定したIDのTodoを更新できること() {
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 指定したIDのTodoを取得できること(){
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 指定したIDのTODOを取得できること() 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