SpringでTestcontainerを利用してQueryDSLをテストしてみよう

業務でQueryDSLを使用していると、実際にそのクエリが正常に動作するかどうかを知るのが難しい場合があります。
このとき、Testcontainerを利用すると、コンテナ環境で実際の環境と同じDBを起動し、そのクエリを実行して結果を対照することでテストを進めることができます。
なぜQueryDSLのテストを行うのか?
実際DBにアクセスしてクエリを実行することは、QueryDSLだけでなく、JPA、MyBatisなどさまざまなORMフレームワークでも可能です。
ただし、JPAのクエリメソッドの場合、使い方が非常に簡単であるため、また既にテストがされていると仮定するため、実際のSQLを扱うQueryDSLや、MyBatisでテストを行う方が多いです。
Testcontainerとは?

TestcontainerはDockerを利用してテスト環境を構築できるようにするライブラリです。
H2のような組み込みDBのDialectを使用しているDBに合わせてテストを行うこともできますが、実際に使用するDBと同じ環境ではないため、意図しないエラーが発生する可能性があります。
Testcontainerを利用すると、実際に使用しているDBをDockerで起動してテストを行うことができるため、設定さえ注意して行えば、実際の環境と同じ環境でテストを行うことができます。(ただし、H2のような軽量な組み込みDBに比べ、テスト速度が遅い場合があります。)
Testcontainerとの連携
Testcontainerを使用するためには、build.gradleに以下のような依存性を追加します。
私はMySQLを使用しているので、MySQLを使用したテスト依存性を追加しました。
dependencies {
// ...
testImplementation 'org.testcontainers:junit-jupiter:1.20.0'
testImplementation 'org.testcontainers:mysql:1.20.0'
// ...
}QueryDslTestConfigの定義
QueryDslテストで使用するQueryDslTestConfigを定義します。
@TestConfiguration
public class QueryDslTestConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}このConfigはテスト環境でQueryDSLが使用するEntityManagerを注入してJPAQueryFactoryを生成します。
QueryDslTestクラスの定義
その後、コンテナを毎回設定してテストを進める手間を減らすため、抽象クラスQueryDslTestを定義します。
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(QueryDslTestConfig.class)
@ActiveProfiles("test")
@SuppressWarnings("resource")
public abstract class QueryDslTest {
@Container
public static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.32")
.withDatabaseName("test_db")
.withUsername("test")
.withPassword("test")
}@DataJpaTestはJPA関連設定のみをロードし、@TestcontainersはTestcontainerを使用可能にします。
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)はDataSourceを使用しないように設定します。(後で@DynamicPropertySourceでコンテナ設定をするため、DataSourceを代替する必要はありません。)
@Import(QueryDslTestConfig.class)はQueryDslTestConfigをロードします。
@ActiveProfiles("test")はapplication-test.ymlを使用するように設定します。
@SuppressWarnings("resource")はコンテナ使用時に発生する警告を無視します。
@Containerはコンテナを定義します。
スキーマを初期設定する必要がある場合は、MySQLContainerのwithInitScriptを使用して初期スキーマを設定できます。
@Container
public static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.32")
// ...
.withInitScript("init.sql");テストコード作成のための準備
その後、テストコードを上で定義したQueryDslTestを継承して必要なテストの準備をします。
class StudentRepositoryImplTest extends QueryDslTest {
@Autowired
StudentRepository studentRepository;
@DynamicPropertySource
static void registerMySQLProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.datasource.driver-class-name", () -> "com.mysql.cj.jdbc.Driver");
}@DynamicPropertySourceを使用すると、動的にDataSourceにコンテナの情報を注入できます。これは一般にapplication.ymlに明示する値と同じ内容で、テストコンテナの特性上、継続的に生成・削除されるため、@DynamicPropertySourceを使用して動的に注入するためです。
この方法が気に入らない場合は、application-test.ymlにコンテナ情報を注入して一つのコンテナでテストを試してみることもできますが、テーブルの作成・削除作業やテスト間の隔離についての考慮が必要です。
テストコードの作成
その後、テストコードを作成します。
class StudentRepositoryImplTest extends QueryDslTest {
// ...
@BeforeEach
void setUp() {
studentRepository.save(Student.builder()
.name("test_name")
.age(20)
.build());
}
@Test
void findStudent() {
// given
Student student = Student.builder()
.name("test_name")
.age(20)
.build();
studentRepository.save(student);
// when
List<Student> students = studentRepository.findStudent("test_name");
// then
assertThat(students).isNotEmpty();
assertThat(students.get(0).getName()).isEqualTo("test_name");
}
@AfterEach
void tearDown() {
studentRepository.deleteAll();
}
}名前を基準に学生を探す単純なテストコードです。
@BeforeEachでテスト前にデータを挿入し、@AfterEachでテスト後にデータを削除します。
このようにテストを進めると、実際の環境と同じ環境でQueryDslで作成されたコードに対してテストを行うことができるようになります。