If you’re using JPA, you can spin up just the Repository layer with @DataJpaTest. Many developers try to test MyBatis Mappers the same way and go looking for @MybatisTest — but no such official annotation exists. The common result is falling back to @SpringBootTest, which boots the entire application and makes tests heavy.
This article explains why no dedicated annotation exists, then compares four actual approaches with real code: a minimal @SpringBootTest setup, a lightweight Java Config, H2, and Testcontainers.
Why MyBatis Has No Equivalent to @DataJpaTest
@DataJpaTest works because JPA-side types like EntityManager and Spring Data’s Repository are Beans that the framework itself recognizes and can auto-configure as a group. Spring Data implements the interfaces itself, and the set of Beans to load is defined by the framework. For a concrete example on the JPA side, see the article on DataJpaTest Repository slice testing — reading them side by side should make the contrast clearer.
MyBatis Mapper interfaces, on the other hand, have no implementation of their own — SqlSession generates a dynamic proxy for an interface that has no concrete body. This binding only comes into existence once the package specified via @MapperScan is scanned through a SqlSessionFactory. In other words, to isolate “just the Mapper,” you need at minimum a DataSource and a SqlSessionFactory as the foundation, which makes it hard to define a generic slice the way JPA does.
As a result, with MyBatis you end up choosing a practical alternative: assembling just the scope of @MapperScan yourself.
Setup ① A Minimal @SpringBootTest + @Transactional Configuration
The easiest option is this one. Since it boots the entire application context, it’s simple to write.
@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]");
}
}
Adding @Transactional rolls the transaction back at the end of the test, so there’s no risk of leaving data behind. However, since Controllers, Services, and every other Mapper get started too, startup time becomes non-negligible as the number of Mappers grows. For small projects or PoCs, this is good enough.
Setup ② A Lightweight Java Config That Loads Only @MapperScan
All you really need is a DataSource, a SqlSessionFactory, and the target Mapper. Prepare a test-only Config class and load only that.
@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);
}
}
If you forget PlatformTransactionManager, Spring will fail to resolve a transaction manager Bean the moment you add @Transactional on the test side, so registering DataSourceTransactionManager is mandatory. Tests that load this config just narrow the target with @ContextConfiguration. Since @SpringBootTest isn’t used, no web server or other auto-configuration starts at all.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = MapperTestConfig.class)
@Transactional
class UserMapperLightweightTest {
@Autowired
private UserMapper userMapper;
@Test
void findById_returnsNull_whenNotExists() {
assertThat(userMapper.findById(999L)).isNull();
}
}
The more Mappers a project accumulates, the more this setup’s speed pays off, since what gets started is narrowed down to just the DataSource and the target Mapper. The actual speedup varies by environment and Mapper count, so treat this as a rough guideline and measure it on your own project.
Trying It with an H2 In-Memory Database
For either Setup ① or ②, using H2 as the test database is the standard choice. Point application-test.yml at H2 and prepare tables with schema.sql. Instead of the EmbeddedDatabaseBuilder shown in Setup ②, you can consolidate everything into this config file instead.
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL
driver-class-name: org.h2.Driver
sql:
init:
mode: always
Even with a compatibility mode like MODE=PostgreSQL, it can’t reproduce custom functions or upsert syntax (such as the fine-grained behavior of ON CONFLICT) used in production. It’s safest to treat this simply as “a database that starts fast and gives quick feedback during development.”
A Real-Database Setup Using Testcontainers
If your logic depends on SQL dialect specifics, you’ll want to test against the same database engine used in production. That’s where Testcontainers comes in. For setup details, see the introductory article on Testcontainers; here’s the minimal setup for a Mapper test.
@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();
}
}
Since Docker is required, startup is slower. You can also combine this with the @ContextConfiguration from Setup ② to use Testcontainers alone without booting the full application. If you want to verify production database locking behavior itself, such as with optimistic locking, it’s also worth reading the MyBatis optimistic locking implementation guide — it will help you judge how far you can get away with H2.
Comparing the Four Setups
| Setup | Startup Speed | Production DB Compatibility | CI Cost |
|---|---|---|---|
| @SpringBootTest + H2 | Slow | Low | No Docker needed |
| Lightweight Config + H2 | Fast | Low | No Docker needed |
| @SpringBootTest + Testcontainers | Slow | High | Docker required |
| Lightweight Config + Testcontainers | Moderate | High | Docker required |
Startup speed is determined by two factors: whether the whole application starts, and whether a Docker container needs to warm up. Production DB compatibility is determined purely by whether the database engine itself matches, so note that H2-based setups are always on the lower end.
Which Should You Choose?
If you just want to quickly check a Mapper’s SQL during development, the Lightweight Config + H2 combination is the least stressful option. On the other hand, for Mappers that use production-specific syntax such as ON CONFLICT or JSON type operators, H2 can’t verify that, so switch to Testcontainers.
In practice, a hybrid approach is a realistic choice too: run fast with Lightweight Config + H2 during development, then switch some tests to Testcontainers for pre-merge CI checks. For the Mapper implementation itself, see the MyBatis implementation guide, and for Service layer testing basics, see Testing Fundamentals with JUnit and Mockito.
Summary
There’s no silver bullet like @DataJpaTest for MyBatis Mappers. This comes down to a structural reason: Mappers are tightly coupled to SqlSession’s dynamic proxy. Given that, a practical approach is to start with a minimal @SpringBootTest setup, move to a lightweight Java Config once startup time becomes a concern, and switch to Testcontainers once you start relying on production-specific DB syntax — reselecting your setup as the project grows. If you’re curious about the design philosophy differences from JPA, also check out the MyBatis vs. JPA comparison article.