SpringBootTest.WebEnvironment.RANDOM_PORTを使用しつつテスト時にDBをロールバック

仕事でJUnit5を使用しつつテストコードを実装していたが、@Transactional を使用してもテスト後にDBがロールバックしない問題にぶつかってしまった。

spring.pleiades.io

リファレンスを見る限りだと @Transactional を使えばロールバックできるはずなので原因が分からないままだったが、別のドキュメントを見る限りテストで RANDOM_PORT を使用しているのが原因なのが分かった。

spring.pleiades.io

これを使うとサーバー側のスレッドとリクエストを投げるスレッドが別々で起動するため、 @Transactional を使ってもロールバックしない状態になるらしい。なんとなく別スレッドで起動するんだろうなとは思いつつドキュメントを見つけられない以上よく分からないままだったが、上記ドキュメントで一応理解することが出来た。

では RANDOM_PORT を使用すると絶対にロールバックできないのかというと別にそうでもないらしく、 DirtiesContext というのを使えば一応は出来るらしい。

DirtiesContext (Spring Framework 5.3.21 API)

こいつを使うとテスト後にコンテキストを再ロードしたりすることが出来るらしく、これにより無理やりDBをロールバックさせることが出来た。以下は確認に使用した仮のテストコード(サーバー側のコードは省略)。

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CustomerControllerTest {
    @LocalServerPort
    private int port;
    @Autowired
    private TestRestTemplate testRestTemplate;

    @DisplayName("DBロールバック確認テスト")
    @Nested
    @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
    class Save {
        @Test
        public void case1() {
            String url = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port)
                    .pathSegment("api", "customers").toUriString();
            CreateCustomerRequest request = new CreateCustomerRequest("firstName1", "lastName1");
            testRestTemplate.postForEntity(url, request, CustomerResponse.class);

            RequestEntity<Void> requestEntity = RequestEntity.get(url).build();
            ResponseEntity<List<CustomerResponse>> responseEntity = testRestTemplate.exchange(requestEntity,
                    new ParameterizedTypeReference<>() {});
            System.out.println(responseEntity.getBody());
            Assertions.assertNotNull(responseEntity.getBody());
            Assertions.assertEquals(1, responseEntity.getBody().size());
        }

        @Test
        public void case2() {
            String url = UriComponentsBuilder.fromHttpUrl("http://localhost").port(port)
                    .pathSegment("api", "customers").toUriString();
            CreateCustomerRequest request = new CreateCustomerRequest("firstName2", "lastName2");
            testRestTemplate.postForEntity(url, request, CustomerResponse.class);

            RequestEntity<Void> requestEntity = RequestEntity.get(url).build();
            ResponseEntity<List<CustomerResponse>> responseEntity = testRestTemplate.exchange(requestEntity,
                    new ParameterizedTypeReference<>() {});
            System.out.println(responseEntity.getBody());
            Assertions.assertNotNull(responseEntity.getBody());
            Assertions.assertEquals(1, responseEntity.getBody().size());
        }
    }
}

@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)コメントアウトすると2つ目のテストコード実行時に CustomerResponse が2つになって返ってくる = 前のテストで保存した情報が残っているのが分かるが、コメントアウトせずに実行すると直前に保存したものだけが返ってくるようになる。

ただ問題としてコンテキストの再ロードには時間がかかるらしく基本的にあまり使用するものではないと思うので、これを利用する場合は対象を限定して実行するか初めからロールバックを期待しないテストコードを書くのが望ましいだろう。