JPAなら @DataJpaTest を付けるだけでRepository層だけを軽く起動できますよね。同じ感覚でMyBatisのMapperをテストしようとして @MybatisTest を探した人、少なくないはずです。でも公式にはそんなアノテーションは存在しません。結局 @SpringBootTest でアプリ全体を起動する重いテストになってしまっている、というのはよく聞く話です。

この記事では、なぜ専用アノテーションがないのかを押さえたうえで、@SpringBootTest 最小構成・軽量Java Config・H2・Testcontainersの4パターンを実際のコードで比較します。

なぜMyBatisには@DataJpaTest相当がないのか

@DataJpaTest は、EntityManager やSpring Data の Repository といった、JPA側が型として把握しているBeanだけをまとめて自動構成できるから成立しています。インターフェースを実装するのはSpring Data自身で、ロードすべきBean群がフレームワーク側で定義されているわけです。JPA側の実例は DataJpaTestによるRepositoryスライステストの記事 で詳しく扱っているので、対比しながら読むと理解しやすいと思います。

一方MyBatisのMapperインターフェースは、実体を持たないインターフェースに対して SqlSession が動的プロキシを生成したものです。この結びつきは @MapperScan で指定したパッケージを SqlSessionFactory 経由でスキャンして初めて成立します。つまり「Mapperだけを切り出す」ためには、最低でも DataSource と SqlSessionFactory という土台が必要で、JPAのように汎用的なスライス定義を用意しづらいんですよね。

結果として、MyBatisでは「@MapperScan の範囲だけを自分で組み立てる」という現実的な代替構成を選ぶことになります。

構成① @SpringBootTest + @Transactionalの最小構成

一番手軽なのはこれです。アプリケーションコンテキストを全部起動してしまうので、書くのは簡単です。

@SpringBootTest
@Transactional
class UserMapperSpringBootTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void insertAndFindById_returnsSavedUser() {
        User user = new User(null, "[email protected]", "太郎");
        userMapper.insert(user);

        User found = userMapper.findById(user.getId());

        assertThat(found.getEmail()).isEqualTo("[email protected]");
    }
}

@Transactional を付けておけばテスト終了時にロールバックされるので、データを汚す心配はありません。ただしController・Service・他のMapperまで全部起動するため、Mapperが増えるほど起動時間が無視できなくなってきます。小規模なプロジェクトやPoCにはこれで十分です。

構成② @MapperScanだけを読み込む軽量Java Config

必要なのは DataSource と SqlSessionFactory、そして対象のMapperだけです。テスト専用のConfigクラスを用意して、そこだけをロードしましょう。

@Configuration
@MapperScan(basePackageClasses = UserMapper.class)
public class MapperTestConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("schema.sql")
                .build();
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        return factoryBean.getObject();
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

PlatformTransactionManager を忘れると、テスト側で @Transactional を付けた瞬間にSpringがトランザクションマネージャBeanを解決できずに失敗するので、DataSourceTransactionManager の登録は必須です。これを読み込むテストは @ContextConfiguration で対象を絞るだけです。@SpringBootTest を使わないので、Webサーバーや他のAuto Configurationは一切起動しません。

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = MapperTestConfig.class)
@Transactional
class UserMapperLightweightTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void findById_returnsNull_whenNotExists() {
        assertThat(userMapper.findById(999L)).isNull();
    }
}

Mapperが増えてきたプロジェクトほど、起動対象が DataSource と対象Mapperだけに絞られるこの構成の速さが効いてきます。具体的な短縮率は環境やMapper数によって変わるので、あくまで目安として捉えつつ、自分のプロジェクトで一度計測してみるのがおすすめです。

H2インメモリDBで試す

構成①②のどちらでも、テスト用DBとしてH2を使うのが定番です。application-test.yml でH2を向け、schema.sql でテーブルを用意します。構成②で紹介した EmbeddedDatabaseBuilder の代わりに、こちらの設定ファイルに一元化してもかまいません。

spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=PostgreSQL
    driver-class-name: org.h2.Driver
  sql:
    init:
      mode: always

MODE=PostgreSQL のような互換モードを付けても、本番で使っている独自関数やupsert構文(ON CONFLICT の細かい挙動など)までは再現できません。あくまで「起動が速く、開発中に素早くフィードバックを得るためのDB」と捉えておくのが安全です。

Testcontainersを使った実DB構成

SQL方言に依存するロジックがあるなら、本番と同じDBエンジンでテストしたいですよね。そこで使うのがTestcontainersです。セットアップの詳細は Testcontainersの基礎記事 に譲りますが、Mapperテストとしての最小構成はこうなります。

@SpringBootTest
@Testcontainers
class UserMapperTestcontainersTest {

    @Container
    static PostgreSQLContainer<?> postgres =
            new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void overrideProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserMapper userMapper;

    @Test
    void insert_usesPostgresSpecificUpsert() {
        userMapper.upsert(new User(null, "[email protected]", "太郎"));
        assertThat(userMapper.findByEmail("[email protected]")).isNotNull();
    }
}

Dockerが必要になる分、起動は遅くなります。構成②の @ContextConfiguration と組み合わせれば、アプリ全体を起動せずにTestcontainersだけ併用することも可能です。楽観的ロックのように本番DBのロック挙動そのものを検証したいケースでは、MyBatisの楽観的ロック実装ガイド も合わせて読んでおくと、どこまでH2で妥協できるかの判断がしやすくなります。

4構成の比較

構成起動速度本番DB互換性CI実行コスト
@SpringBootTest + H2遅い低いDocker不要
軽量Config + H2速い低いDocker不要
@SpringBootTest + Testcontainers遅い高いDocker必要
軽量Config + Testcontainers中程度高いDocker必要

起動速度は「アプリ全体を起動するか」「DockerコンテナをWarm upするか」の2軸で決まります。本番DB互換性はDBエンジンそのものの一致で決まるので、H2系は常に低めになる点に注意してください。

どちらを選ぶべきか

開発中にMapperのSQLを素早く確認したいだけなら、軽量Config + H2の組み合わせが一番ストレスがありません。一方で、ON CONFLICT やJSON型の演算子など本番DB固有の構文を使っているMapperは、H2では検証できないのでTestcontainersに切り替えましょう。

実際の運用では、開発中は軽量Config + H2で素早く回し、CIのマージ前チェックでは一部をTestcontainersに切り替えるハイブリッド運用も現実的な選択です。Mapperの実装そのものについては MyBatisの実装ガイド、Service層のテストは JUnitとMockitoによるテストの基本 も参考にしてみてください。

まとめ

MyBatisのMapperには @DataJpaTest のような銀の弾丸は用意されていません。これはMapperがSqlSessionの動的プロキシに強く結合しているという構造上の理由によるものです。そのぶん、@SpringBootTest の最小構成から始めて、起動時間が気になってきたら軽量Java Configへ、本番DB固有の構文を使うようになったらTestcontainersへと、プロジェクトの成長に合わせて構成を選び直していくのが現実的なアプローチです。JPAとの設計思想の違いが気になる人は MyBatisとJPAの比較記事 も合わせて読んでみてください。