Preloaded data for testing.

Static Badge Gradle Plugin Portal Version GitHub License GitHub Actions Workflow Status GitHub Repo stars

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.