How to test the Data Layer of your Spring Boot Application with @DataMongoTest


In this Blogpost, I will show you how to test the MongoDB-based data layer of your Spring Boot Application with @DataMongoTest. While having a look at an example, we will bootstrap some test data, use an embedded in-memory MongoDB and write an actual MongoDB test.

What is @DataMongoTest?

@DataMongoTest is an annotation that is used to test the MongoDB components of our application. Applied to a test class it will disable the full-auto-configuration and instead configure only those components, that are relevant for MongoDB tests. For example MongoRepository, ReactiveMongoRepository, MongoClient, and MongoTemplate. It will also scan our project for classes with the @Documents annotation. If there is an embedded in-memory database on the classpath, it will be used for the test.

Test Setup

To use an in-memory MongoDB for our tests, we need to add de.flapdoodle.embed:de.flapdoodle.embed.mongo as a dependency to our project.

pom.xml
<dependency>
    <groupId>de.flapdoodle.embed</groupId>
    <artifactId>de.flapdoodle.embed.mongo</artifactId>
    <scope>test</scope>
</dependency>

Since Spring Boot 2.6.x it is also mandatory to configure the version of the MongoDB that should be used. We do this by adding the following property:

application.properties
spring.mongodb.embedded.version=3.6.5

You can find all supported versions in the de.flapdoodle.embed.mongo.distribution.Version enum.
If you use a Spring Boot Version prior to 2.6.0 you don’t have to configure this property. In this case MongoDB 3.5.5 will be used.

The Example

In this example, we have Restaurants with a Geo-Location. For a specific Use-Case, we need to find all restaurants within the vicinity of a specific point. Here are the classes of our documents:

Restaurant.java
@Document
public class Restaurant {

    @Id
    private String id;
    private String name;
    private Coordinate location;

    // constuctor, getter and setter omitted
}
Coordinate.java
public record Coordinate(double latitude, double longitude) {

    public Coordinate {
        if (longitude < -180 || longitude > 180) {
            throw new IllegalArgumentException("Longitude must be between -180 and 180.");
        }

        if (latitude < -90 || latitude > 90) {
            throw new IllegalArgumentException("Latitude must be between -180 and 180.");
        }
    }
}

This repository contains the logic to retrieve the restaurants within in specific vicinity which we want to test:

RestaurantRepository.java
@Repository
public interface RestaurantRepository extends MongoRepository<Restaurant, String> {

    List<Restaurant> findByLocationWithin(Circle circle);

}

Bootstrapping Test Data

Regardless of what kind of automated test we write, there is always one task we have to do before executing the thing we want to test – put the system into a known state. Often this phase in the lifecycle of a test is called Setup- or Bootstrapping phase. In a unit test, this is done by instantiating some objects. In the kind of integration test, we are going to write, it also means putting some test data into our database. Luckily for us, Spring Boot supports some different ways to do this.

Using the MongoTemplate

As mentioned above Spring Boot will configure a MongoTemplate for tests with the @DataMongoTest annotation. This means we can use it to bootstrap the data required for our test.

RestaurantRepositoryTest.java
@DataMongoTest
class RestaurantRepositoryTest {

    @Autowired
    private MongoTemplate mongoTemplate;

    @Test
    void bootstrapTestDataWithMongoTemplate() {
        var restaurant = new Restaurant("Café Java", new Coordinate(52.52439, 13.41043));
        mongoTemplate.insert(restaurant);

        // ...
    }
}

Using SpringData Repositories

We can also use the MongoRepositories of our application to bootstrap the required test data.

RestaurantRepositoryTest.java
@DataMongoTest
class RestaurantRepositoryTest {

    @Autowired
    private RestaurantRepository restaurantRepository;

    @Test
    void bootstrapTestDataWithMongoRepository() {
        var restaurant = new Restaurant("Café Java", new Coordinate(52.52439, 13.41043));
        restaurantRepository.save(restaurant);

        // ...
    }
}

This approach has the disadvantage that you rely on the functionality of the component you want to test. I suggest using this method only if you already know that your save method works as expected.

Using a Jackson2RepositoryPopulatorFactoryBean

The last method to bootstrap our test data is to provide a Jackson2RepositoryPopulatorFactoryBean. This Bean will read one or more JSON files to populate the repositories of your application.

RestaurantPopulatorConfiguration.java
@Configuration
public class RestaurantPopulatorConfiguration {

    @Bean
    public Jackson2RepositoryPopulatorFactoryBean getRepositoryPopulator() {
        Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean();
        factory.setResources(new Resource[]{new ClassPathResource("restaurant-data.json")});

        return factory;
    }

}
/src/test/resources/restaurant-data.json
[
    {
        "_class": "dev.jschmitz.datamongotest.Restaurant",
        "name": "Java Café",
        "location": {
            "latitude": "52.52439",
            "longitude": "13.41043"
        }
    }
]

To use the configuration in our integration test we need to add an @Import annotation to our test class:

RestaurantRepositoryWithDataPopulatorTest.java
@DataMongoTest
@Import(RestaurantPopulatorConfiguration.class)
class RestaurantRepositoryWithDataPopulatorTest {
    //...
}

Writing the Test

The test case we are going to write should prove that RestaurantRepository::findByLocationWithin() retrieves only those restaurants, within a specified vicinity of a point. In this test, we are using a point in Berlin and a search radius of 2KM.

RestaurantRepositoryTest.java
@Test
void findByLocationWithin_FindsRestaurantsWithinAGivenDistance() {
    var circle = new Circle(new Point(52.52437, 13.41053), new Distance(2, Metrics.KILOMETERS));
    // ...
}

To put our system in a known state we will persist the following restaurants to the MongoDB:

  • “Café Java” at the location 52.52439, 13.41043
  • “Spring Restaurant” at the location 52.52447, 13.41024
  • “Jakarta Restaurant” at the location 52.52447, 13.41022

With this data, we expect to retrieve 2 restaurants from our method – “Café Java” and “Spring Restaurant”. The “Jakarta Restaurant” should not be included because it is located slightly out of the search radius we defined. Our test code should now look something like this:

RestaurantRepositoryTest.java
@Test
void findByLocationWithin_FindsRestaurantsWithinAGivenDistance() {
    var circle = new Circle(new Point(52.52437, 13.41053), new Distance(2, Metrics.KILOMETERS));

    var restaurant = new Restaurant("Café Java", new Coordinate(52.52439, 13.41043));
    var restaurant2 = new Restaurant("Spring Restaurant", new Coordinate(52.52447, 13.41024));
    var restaurant3 = new Restaurant("Jakarta Restaurant", new Coordinate(52.52447, 13.41022));

    mongoTemplate.insert(restaurant);
    mongoTemplate.insert(restaurant2);
    mongoTemplate.insert(restaurant3);

    assertEquals(2, restaurantRepository.findByLocationWithin(circle).size());
}

Teardown after the Test

When writing any kind of automated test, we need to make sure that all tests are independent of one another. If one test writes data into the database, it could potentially break the next test which does not expect any existing data. Therefore, we need to delete the data that is created by our tests. Unlike the @DataJpaTest, the @DataMongoTest we are using here is not transactional. This means the data persisted within a test will not be automatically deleted. The easiest way to do this that I found is to drop the database after each test. We could do this explicitly in each test or once in a method that is annotated with JUnit’s @AfterEach.

RestaurantRepositoryTest.java
@AfterEach
void cleanUpDatabase() {
    mongoTemplate.getDb().drop();
}

I prefer to delete the data within the @AfterEach annotated method. This way, no developer can accidentally forget to do it while implementing a new test.

Resources