From a46c06b61eedbf3bc59411d89cca6eb9feea01ef Mon Sep 17 00:00:00 2001 From: Rashidi Zin Date: Sun, 24 Nov 2024 20:15:57 +0800 Subject: [PATCH] Replace loading data through mongo-init.js with RepositoryPopulator --- data-mongodb-tc-data-load/README.adoc | 110 +++++++++++++----- data-mongodb-tc-data-load/build.gradle | 1 + .../tc/dataload/user/UserRepositoryTests.java | 32 +++-- .../src/test/resources/mongo-init.js | 6 - .../src/test/resources/users.json | 5 + 5 files changed, 111 insertions(+), 43 deletions(-) delete mode 100644 data-mongodb-tc-data-load/src/test/resources/mongo-init.js create mode 100644 data-mongodb-tc-data-load/src/test/resources/users.json diff --git a/data-mongodb-tc-data-load/README.adoc b/data-mongodb-tc-data-load/README.adoc index 7bdf0ffd..07a1605a 100644 --- a/data-mongodb-tc-data-load/README.adoc +++ b/data-mongodb-tc-data-load/README.adoc @@ -1,7 +1,7 @@ = Spring Data MongoDb with Testcontainers :source-highlighter: highlight.js Rashidi Zin -2.0, October 12, 2023 +3.0, November 24, 2024: Replace usage of mongo-init.js with Spring's RepositoryPopulator :toc: :nofooter: :icons: font @@ -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; @@ -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. diff --git a/data-mongodb-tc-data-load/build.gradle b/data-mongodb-tc-data-load/build.gradle index d82e65da..6543e679 100644 --- a/data-mongodb-tc-data-load/build.gradle +++ b/data-mongodb-tc-data-load/build.gradle @@ -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') { diff --git a/data-mongodb-tc-data-load/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java b/data-mongodb-tc-data-load/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java index 846b15ed..db38c432 100644 --- a/data-mongodb-tc-data-load/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java +++ b/data-mongodb-tc-data-load/src/test/java/zin/rashidi/data/mongodb/tc/dataload/user/UserRepositoryTests.java @@ -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; @@ -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; + } + } + } diff --git a/data-mongodb-tc-data-load/src/test/resources/mongo-init.js b/data-mongodb-tc-data-load/src/test/resources/mongo-init.js deleted file mode 100644 index 3210adec..00000000 --- a/data-mongodb-tc-data-load/src/test/resources/mongo-init.js +++ /dev/null @@ -1,6 +0,0 @@ -db.createCollection("user"); - -db.user.insert({ - "name": "Rashidi Zin", - "username": "rashidi.zin" -}); diff --git a/data-mongodb-tc-data-load/src/test/resources/users.json b/data-mongodb-tc-data-load/src/test/resources/users.json new file mode 100644 index 00000000..bf6d704e --- /dev/null +++ b/data-mongodb-tc-data-load/src/test/resources/users.json @@ -0,0 +1,5 @@ +[{ + "_class": "zin.rashidi.data.mongodb.tc.dataload.user.User", + "name": "Rashidi Zin", + "username": "rashidi.zin" +}] \ No newline at end of file