Background
It is common to have preloaded data when we are testing our delete, read, and update operations. One of the most common approaches is to load them programmatically. In this example, we will see how we can use Testcontainers to load data into our MongoDB instance.
Load Data Programmatically
It is common that we load data programmatically. There are several approaches to do this.
Using @BeforeEach
and @AfterEach
@BeforeEach
will be executed before each test method execution while @AfterEach
will be executed after test method executed. In this example,
we will use @BeforeEach
to load data and @AfterEach
to delete data.
@DataMongoTest
@Testcontainers
class UserRepositoryTests {
@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest");
@Autowired
private UserRepository repository;
@BeforeEach
void create() {
repository.save(new User(null, "rashidi.zin", "Rashidi Zin"));
}
@AfterEach
void delete() {
repository.deleteAll();
}
@Test
@DisplayName("Given there is a user with username rashidi.zin and name Rashidi Zin When I search for username rashidi.zin Then user with provided username should be returned")
void findByUsername() {
var user = repository.findByUsername("rashidi.zin");
assertThat(user)
.extracting("name")
.isEqualTo("Rashidi Zin");
}
@Test
@DisplayName("Given there is no user with username zaid.zin When I search for username zaid.zin Then null should be returned")
void findByUsernameWithNonExistingUsername() {
var user = repository.findByUsername("zaid.zin");
assertThat(user).isNull();
}
}
While this approach works, it might be time-consuming when we have many methods to be executed. Another approach is to use @TestExecutionListeners
Using @TestExecutionListeners
@TestExecutionListeners
allows us to register implemented TestExecutionListener
which can be used to execute code before and after test execution.
The following TestExecutionListener
is used to load data before executing the test class and remove all data after test class has been executed.
class UserTestExecutionListener extends AbstractTestExecutionListener {
private User user;
@Override
public void beforeTestClass(TestContext testContext) {
var mongo = testContext.getApplicationContext().getBean(MongoOperations.class);
user = mongo.insert(new User(null, "rashidi.zin", "Rashidi Zin"));
}
@Override
public void afterTestClass(TestContext testContext) {
var mongo = testContext.getApplicationContext().getBean(MongoOperations.class);
mongo.remove(user);
}
}
Then we will include it in our test class:
@DataMongoTest
@Import(UserRepositoryTests.TestcontainersConfiguration.class)
@TestExecutionListeners(listeners = UserTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
class UserRepositoryTests {
@Autowired
private UserRepository repository;
@Test
@DisplayName("Given there is a user with username rashidi.zin and name Rashidi Zin When I search for username rashidi.zin Then user with provided username should be returned")
void findByUsername() {
var user = repository.findByUsername("rashidi.zin");
assertThat(user)
.extracting("name")
.isEqualTo("Rashidi Zin");
}
@Test
@DisplayName("Given there is no user with username zaid.zin When I search for username zaid.zin Then null should be returned")
void findByUsernameWithNonExistingUsername() {
var user = repository.findByUsername("zaid.zin");
assertThat(user).isNull();
}
@TestConfiguration(proxyBeanMethods = false)
@ImportAutoConfiguration(TestcontainersPropertySourceAutoConfiguration.class)
static class TestcontainersConfiguration {
@Bean
MongoDBContainer mongoDbContainer(DynamicPropertyRegistry registry) {
var mongo = new MongoDBContainer("mongo:latest");
registry.add("spring.data.mongodb.uri", mongo::getReplicaSetUrl);
return mongo;
}
}
}
In this example, we are using @TestExecutionListeners
to register UserTestExecutionListener
which will be executed before and after test class execution. Alternatively, we also no longer utilise on
helpful annotations - @Testcontainers
, @Container
, and @ServiceConnection
.
Load Data Using RepositoryPopulators
Next approach is to load data using RepositoryPopulators and Testcontainers. We will start by creating users.json and populate it with the following content.
[{
"_class": "zin.rashidi.data.mongodb.tc.dataload.user.User",
"name": "Rashidi Zin",
"username": "rashidi.zin"
}]
First, we will have to add jackson-databind
as our dependency in build.gradle.
dependencies {
testImplementation "com.fasterxml.jackson.core:jackson-databind"
}
Next we will create a @TestConfiguration
class which will define RepositoryPopulator
.
class UserRepositoryTests {
@TestConfiguration
static class RepositoryPopulatorTestConfiguration {
@Bean
public Jackson2RepositoryPopulatorFactoryBean jacksonRepositoryPopulator() {
var populator = new Jackson2RepositoryPopulatorFactoryBean();
populator.setResources(new Resource[] { new ClassPathResource("users.json") });
return populator;
}
}
}
Then we will inform UserRepositoryTests to include RepositoryPopulatorTestConfiguration
.
@DataMongoTest(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = UserRepositoryTests.RepositoryPopulatorTestConfiguration.class))
class UserRepositoryTests {
@TestConfiguration
static class RepositoryPopulatorTestConfiguration {
@Bean
public Jackson2RepositoryPopulatorFactoryBean jacksonRepositoryPopulator() {
var populator = new Jackson2RepositoryPopulatorFactoryBean();
populator.setResources(new Resource[] { new ClassPathResource("users.json") });
return populator;
}
}
}
Finally, the usual setup to include @TestContainers
and MongoDBContainer
.
@Testcontainers
@DataMongoTest(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = UserRepositoryTests.RepositoryPopulatorTestConfiguration.class))
class UserRepositoryTests {
@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest");
@TestConfiguration
static class RepositoryPopulatorTestConfiguration {
@Bean
public Jackson2RepositoryPopulatorFactoryBean jacksonRepositoryPopulator() {
var populator = new Jackson2RepositoryPopulatorFactoryBean();
populator.setResources(new Resource[] { new ClassPathResource("users.json") });
return populator;
}
}
}
Once everything is ready, we will add our tests.
@Testcontainers
@DataMongoTest(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = UserRepositoryTests.RepositoryPopulatorTestConfiguration.class))
class UserRepositoryTests {
@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest");
@Autowired
private UserRepository repository;
@Test
@DisplayName("Given there is a user with username rashidi.zin and name Rashidi Zin When I search for username rashidi.zin Then user with provided username should be returned")
void findByUsername() {
var user = repository.findByUsername("rashidi.zin");
assertThat(user)
.extracting("name")
.isEqualTo("Rashidi Zin");
}
@Test
@DisplayName("Given there is no user with username zaid.zin When I search for username zaid.zin Then null should be returned")
void findByUsernameWithNonExistingUsername() {
var user = repository.findByUsername("zaid.zin");
assertThat(user).isNull();
}
@TestConfiguration
static class RepositoryPopulatorTestConfiguration {
@Bean
public Jackson2RepositoryPopulatorFactoryBean jacksonRepositoryPopulator() {
var populator = new Jackson2RepositoryPopulatorFactoryBean();
populator.setResources(new Resource[] { new ClassPathResource("users.json") });
return populator;
}
}
}
With that, data will be loaded into MongoDB before the test execution. Full implementation of UserRepositoryTests
:
This also allows us to have a single source of truth in managing data for our tests.