前回 までで一般的な Web アプリケーションを作るのに必要な Controller 部分のサンプルが溜まってきたと思います。あとは UseCase とか Repository が残っていますが、こいつらは Micronaut 依存が少ないので興味がある方は引き続きやっていただければと思います*1。 今回はテスト周りをやってみたいと思います。
コードはこちら (本文のコードは色々端折ってるのでコードをご確認ください)
Form のテスト
ここでは、Web ブラウザからのリクエストを受ける class を Form と呼んでいます。 Bean Validation で validation を行うのであれば、少なくともアノテーション(と設定)を正しく付与していることをテストする必要があります。
Validator
テストクラスはこんな感じです。
@MicronautTest // 1 class TaskFormTest { @Inject Validator validator; // 2 @Test @DisplayName("validate でエラーが起きないケース") void testValidate() { // setup var sut = TaskForm.builder().taskName("name_0001").content("content_001").build(); // exercise var actual = validator.validate(sut); // verify assertThat(actual).isEmpty(); } @Test @DisplayName("null を渡した時") void testValidate_NullValue() { // setup var sut = new TaskForm(); // exercise var actual = validator.validate(sut); // 3 // verify var messages = actual .stream() .map(value -> value.getPropertyPath() + ":" + value.getMessage()) .collect(Collectors.toList()); assertThat(messages).containsOnly("taskName:must not be null", "content:must not be null"); } }
@MicronautTest
をつけることで、@Singleton
をつけた class をテスト側で@Inject
して使用することができます- Validator を Inject します
- エラーがあると Validator#validate の戻り値に設定するので verify します
@Inject
が spring で言う @Autowired
です*2。
Validation の他に必要なテストがあれば追加していきます。
Controller の UnitTest
UnitTest では 依存する UseCase 等は Mock に置き換えて、メソッド呼び出し時を観点としたテストを行います。 Mock ライブラリとしてMockito を使っています。
テストクラスはこんな感じです。
@ExtendWith(MockitoExtension.class) // 1 class AddControllerTest { @Mock private TaskUseCase taskUseCase; // 2 @Mock private ConstraintViolationMessageConverter constraintViolationMessageConverter; @Mock private HttpRequest<Map<String, Object>> request; @InjectMocks AddController sut; // 3 @Test @DisplayName("addTask のテスト.") void testAddTask() { // setup doNothing().when(taskUseCase).createTask(any()); var taskForm = TaskForm.builder().taskName("タスク名").content("内容").build(); // exercise var actual = sut.addTask(taskForm); // verify assertThat(actual) .isInstanceOfSatisfying( NettyMutableHttpResponse.class, response -> { assertThat(response.getHeaders().get(HttpHeaders.LOCATION)).isEqualTo("/tasks"); assertThat(response) .returns(HttpStatus.MOVED_PERMANENTLY, NettyMutableHttpResponse::getStatus); }); verify(taskUseCase).createTask(taskForm.toCreateTaskUseCaseRequest()); // 4 } }
@MicronautTest
の代わりに@ExtendWith
をつけます- テスト対象 class が Inject している class を
@Mock
で mock 化します @InjectMocks
がついていると@Mock
を付けた class を Inject してインスタンスを生成します- mock 使ったら呼び出し回数や引数の verify をしましょう。そうしないとテストの意味が激減します
アクセス権やログイン済みか?の状態のテストはここでは行いません。Java のロジックとして想定通りかの観点でテストを行います。Micronaut はあまり関係ないです。
IntegrationTest
画面が絡んでくるとどこまでやるべきか悩ましいです。 ひとまず雑に要素の存在チェックをすることにしました。E2E のテストは別でやると思ってます。
@MicronautTest // 1 class AddControllerIntegrationTest { @Inject IntegrationTestHelper integrationTestHelper; // 2 @Inject TaskUseCase taskUseCase; @Test @DisplayName("GET /tasks/add のテスト") void testIndex() throws Exception { // setup var client = integrationTestHelper.login(); // 3 var request = integrationTestHelper.buildGetRequest("/tasks/add"); // exercise var response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); // verify assertResponseWithBody( response, HttpStatus.OK, html -> { assertElementExists(html, "input[name='csrfToken']"); // jsoup でドキュメントの要素を verify assertTextEquals(html, "h3.title.is-3", "Task 登録"); }); } @Test @DisplayName("POST /tasks/add のテスト") void testAdd() throws Exception { assertThat(taskUseCase.allTask()).isEmpty(); // setup var client = integrationTestHelper.login(); var csrfToken = integrationTestHelper.getCsrfToken(client, "/tasks/add"); // 4 var postParameter = "csrfToken=" + csrfToken + "&taskName=task_name_001&content=task_content_001"; var request = integrationTestHelper.buildPostRequest("/tasks/add", postParameter); // exercise var response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); // verify assertResponseWithLocation( response, HttpStatus.MOVED_PERMANENTLY, location -> assertThat(location).isEqualTo("/tasks")); // 永続化していること var actual = taskUseCase.allTask(); assertThat(actual).hasSize(1); var actualTask = actual.get(0); assertThat(actualTask.getTaskId()).isNotNull(); assertThat(actualTask) .returns("task_name_001", TaskUseCaseResult::getTaskName) .returns("task_content_001", TaskUseCaseResult::getContent); } }
@MicronautTest
をつけます。netty が立ち上がります- テスト用に使う Helper を用意しておきます
- login 状態にした client を取得してアクセスします
- CSRF トークンを取得してアクセスします
テスト用に起動した内部サーバとの通信を Running the Embedded Server のように
@Inject @Client("/") HttpClient client;
でやりたかったのですが、今回のサンプルは Session Authentication を組み込んでいるので、ログイン済みの状態を示すために Cookie が必要でした。
なので、io.micronaut.http.client.HttpClient
でなく、java.net.http.HttpClient
を使用することにしました。
また、ログイン成功時の Cookie を保持した HttpClient や CSRF トークンを取得する処理を Helper として定義しています。
IntegrationTest ではブラウザからのリクエストを受けた時の振る舞いを確認します。 なので、
等もこのテストでの確認ポイントになります。
例えば AWS の SDK を mock して...と言う時は、Using Mockito Mocks を参考に Mock にすることができます*3。
テスト用に設定を変えたい時は @Property
や @MicronautTest
の propertySources を指定すれば良さそうです。
まとめ
どこまでテスト書くかは時間との兼ね合いかもしれませんが、テストがあればデグレしてない安心感を得られます。 フレームワーク依存の部分もテストがあれば version up する時に気付きやすくもなります。面倒かもだけどテスト大事です。
こうしてみると Spring でできたことができないケースは少ないと思います。 テストの起動も Spring よりは早い印象を受けます。ちょっとした API サーバはもちろんですが、Micronaut で大体いける気がしてきました。