Skip to content

Commit

Permalink
Replace loading data through mongo-init.js with RepositoryPopulator
Browse files Browse the repository at this point in the history
  • Loading branch information
rashidi authored Nov 24, 2024
1 parent a630c42 commit a46c06b
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 43 deletions.
110 changes: 82 additions & 28 deletions data-mongodb-tc-data-load/README.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
= Spring Data MongoDb with Testcontainers
:source-highlighter: highlight.js
Rashidi Zin <rashidi@zin.my>
2.0, October 12, 2023
3.0, November 24, 2024: Replace usage of mongo-init.js with Spring's RepositoryPopulator
:toc:
:nofooter:
:icons: font
Expand Down Expand Up @@ -147,66 +147,106 @@ class UserRepositoryTests {
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 Testcontainers
Next approach is to load data using `mongo-init.js` and Testcontainers. We will start by inserting data through link:{url-quickref}/src/test/resources/mongo-init.js[mongo-init.js] file.
== Load Data Using RepositoryPopulators
Next approach is to load data using https://docs.spring.io/spring-data/mongodb/reference/repositories/core-extensions.html#core.repository-populators[RepositoryPopulators] and Testcontainers.
We will start by creating link:{url-quickref}/src/test/resources/users.json[users.json] and populate it with the following content.

[source,javascript]
[source,json]
----
db.createCollection("user");
db.user.insert({
[{
"_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 link:${url-quickref}/build.gradle[build.gradle].

[source,groovy]
----
dependencies {
testImplementation "com.fasterxml.jackson.core:jackson-databind"
}
----

Then we will create a `MongoDBContainer` and mount the `mongo-init.js` file into `/docker-entrypoint-initdb.d` directory.
Next we will create a `@TestConfiguration` class which will define `RepositoryPopulator`.

[source,java]
----
@DataMongoTest
@Testcontainers
class UserRepositoryTests {
@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest")
.withCopyToContainer(forClasspathResource("mongo-init.js"), "/docker-entrypoint-initdb.d/mongo-init.js");
@TestConfiguration
static class RepositoryPopulatorTestConfiguration {
@Bean
public Jackson2RepositoryPopulatorFactoryBean jacksonRepositoryPopulator() {
var populator = new Jackson2RepositoryPopulatorFactoryBean();
populator.setResources(new Resource[] { new ClassPathResource("users.json") });
return populator;
}
}
}
----

Next is to inform Testcontainers on how to determine that our MongoDB is ready. This will be determined by the checking that the phrase `waiting for connections`
appeared twice:
Then we will inform link:${url-quickref}/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java[UserRepositoryTests] to include `RepositoryPopulatorTestConfiguration`.

[source,java]
----
@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`.

[source,java]
----
@DataMongoTest
@Testcontainers
@DataMongoTest(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = UserRepositoryTests.RepositoryPopulatorTestConfiguration.class))
class UserRepositoryTests {
@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest")
.withCopyToContainer(forClasspathResource("mongo-init.js"), "/docker-entrypoint-initdb.d/mongo-init.js")
.waitingFor(forLogMessage("(?i).*waiting for connections.*", 1));
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;
}
}
}
----

With that, data will be loaded into MongoDB before the test execution. Full implementation of link:{url-quickref}/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java[`UserRepositoryTests`]:
Once everything is ready, we will add our tests.

[source,java]
----
@DataMongoTest
@Testcontainers
@DataMongoTest(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = UserRepositoryTests.RepositoryPopulatorTestConfiguration.class))
class UserRepositoryTests {
@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest")
.withCopyToContainer(forClasspathResource("mongo-init.js"), "/docker-entrypoint-initdb.d/mongo-init.js")
.waitingFor(forLogMessage("(?i).*waiting for connections.*", 1))
.withStartupAttempts(2)
.withStartupTimeout(ofMinutes(1));
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest");
@Autowired
private UserRepository repository;
Expand All @@ -228,7 +268,21 @@ class UserRepositoryTests {
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 link:{url-quickref}/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java[`UserRepositoryTests`]:

This also allows us to have a single source of truth in managing data for our tests.
1 change: 1 addition & 0 deletions data-mongodb-tc-data-load/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mongodb'
testImplementation "com.fasterxml.jackson.core:jackson-databind"
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,32 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.repository.init.Jackson2RepositoryPopulatorFactoryBean;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static java.time.Duration.ofMinutes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage;
import static org.testcontainers.utility.MountableFile.forClasspathResource;
import static org.springframework.context.annotation.FilterType.ASSIGNABLE_TYPE;

/**
* @author Rashidi Zin
*/
@DataMongoTest
@Testcontainers
@DataMongoTest(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = UserRepositoryTests.RepositoryPopulatorTestConfiguration.class))
class UserRepositoryTests {

@Container
@ServiceConnection
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest")
.withCopyToContainer(forClasspathResource("mongo-init.js"), "/docker-entrypoint-initdb.d/mongo-init.js")
.waitingFor(forLogMessage("(?i).*waiting for connections.*", 1))
.withStartupAttempts(2)
.withStartupTimeout(ofMinutes(1));
private static final MongoDBContainer mongo = new MongoDBContainer("mongo:latest");

@Autowired
private UserRepository repository;
Expand All @@ -49,4 +51,16 @@ void findByUsernameWithNonExistingUsername() {

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;
}
}

}
6 changes: 0 additions & 6 deletions data-mongodb-tc-data-load/src/test/resources/mongo-init.js

This file was deleted.

5 changes: 5 additions & 0 deletions data-mongodb-tc-data-load/src/test/resources/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[{
"_class": "zin.rashidi.data.mongodb.tc.dataload.user.User",
"name": "Rashidi Zin",
"username": "rashidi.zin"
}]

0 comments on commit a46c06b

Please sign in to comment.