Spring Test MongoDb
Tools to write integration tests of applications with a MongoDb persistence layer and Spring Framework.
1. Introduction
SpringTestMongo is a tool to write integration tests on systems which used the MongoDB as a data storage. This library provides you an ability to run your tests with the docker image of MongoDB (by the use of Testcontainers).
Also, spring-test-mongo allows you to write tests in a more pragmatic manner using annotations and extension for JUnit5. Also, this library supports the JUnit4 by the using a Rule approach. You can use JSON files to prepare the state of a database or to check this state after some activities (update, delete e.t.c).
2. Getting started
You need to add the next dependency:
<dependency>
<groupId>com.jupiter-tools</groupId>
<artifactId>spring-test-mongo</artifactId>
<version>0.15</version>
<scope>test</scope>
</dependency>
3. JUnit5 integration test
Let’s look at the example of using the TestContainers library to make integration tests. And the JUnit5 extension to make your tests more pragmatic and elegant.
@MongoDbIntegrationTest (1)
class JUnit5ExampleTest {
@Autowired
private MongoTemplate mongoTemplate;
@Test
@MongoDataSet(value = "/dataset/bar_dataset.json") (2)
void testPopulatingByMongoDataSet() throws Exception {
Bar simpleDoc = mongoTemplate.findById("55f3ed00b1375a48e618300b", Bar.class); (3)
Assertions.assertThat(simpleDoc)
.isNotNull()
.extracting(Bar::getId, Bar::getData)
.containsOnly("55f3ed00b1375a48e618300b", "BB");
}
}
-
This annotation runs MongoDB instance by the use of Testcontainers library and turn on SpringTestMongo which able you to use DataSets in tests
-
Initialize the state of the database from JSON file before the test execute
-
Read data from the MongoDB instance which started by the MongoDbIntegrationTest annotation
Let’s look at the file with a data-set(bar_dataset.json
):
{
"com.antkorwin.springtestmongo.Bar": [
{
"id": "55f3ed00b1375a48e618300a",
"data": "A"
},
{
"id": "55f3ed00b1375a48e618300b",
"data": "BB"
}
]
}
4. JUnit4 integration test
Also, you can use the JUnit4 rule and the base abstract class to write integration tests in the old-style (with the use of the JUnit4 and inheritance in tests)
public class JUnit4ExampleTest extends BaseMongoIT {
@Test
@MongoDataSet(value = "/dataset/multidocument_dataset.json")
public void testPopulatingByMongoDataSet() throws Exception {
Bar simpleDoc = mongoTemplate.findById("55f3ed00b1375a48e618300b", Bar.class);
Assertions.assertThat(simpleDoc)
.isNotNull()
.extracting(Bar::getId, Bar::getData)
.containsOnly("55f3ed00b1375a48e618300b", "BB");
}
}
5. Importing datasets before test execution
spring-test-mongo
provides you an ability to import initial data in the MongoDB database before starting a test execution. You can describe this dataset as a JSON file with an array of MongoDB documents.
For example we consider a simple document class:
@Document
public class Foo {
@Id
private String id;
private Date time;
private int counter;
}
Let’s look at the dataset file (foo-init.json
) with a pare instances of this class:
{
"com.antkorwin.springtestmongo.Foo": [ (1)
{
"id": "77f3ed00b1375a0000000001",
"time":1516527720001,
"counter":1
},{
"id": "77f3ed00b1375a0000000002",
"time":1516527720002,
"counter":2
}
]
}
-
full class-reference of populated document collection
Now we can write an integration test which will init the database from this file:
@Test @MongoDataSet(value = "/dataset/foo-init.json") (1) void testImportByMongoDataSetAnnotation() { // Act Foo fooDoc = mongoTemplate.findById("77f3ed00b1375a0000000001", Foo.class); // Assert Assertions.assertThat(fooDoc) .isNotNull() .extracting(Foo::getCounter, Foo::getTime) .containsOnly(1, new Date(1516527720001L)); }
-
Annotation MongoDataSet initialize the state of MongoDb from JSON file before the test execution
5.1. Multiple document types in one dataset
If you need to populate different documents collection before execution one test then you can write multiple arrays of necessary types in a dataset:
{
"com.antkorwin.springtestmongo.Bar": [ (1)
{
"id": "55f3ed00b1375a48e618300a",
"data": "A"
},
{
"id": "55f3ed00b1375a48e618300b",
"data": "BB"
}
],
"com.antkorwin.springtestmongo.Foo": [ (2)
{
"id": "77f3ed00b1375a48e618300a",
"time":1516527720000,
"counter":1
}
]
}
-
first document array
-
second document array
5.2. Nested documents
You can describe nested objects in your dataset. Let’s look at the the next model:
@Data
@Document
public class FooBar {
@Id
private String id;
private String data;
private Bar bar; (1)
}
@Data
@Document
public class Bar {
@Id
private String id;
private String data;
}
-
nested object with another type
so, you can describe a dataset for this example as shown below:
{
"com.jupiter.tools.spring.test.mongo.FooBar" : [ {
"id": "55f1dd90a1246a44e118300b",
"data" : "TOP LEVEL DATA",
"bar": {
"id": "88f3ed00b1375a48e619900c",
"data":"NESTED DATA"
}
}]
}
5.3. Date and time in dataset
To set a date and time value in a field you can use the next syntactic construction:
For example if you need tomorrow you can use this pattern: [NOW]+1(DAYS)
, if you need a time value of three minutes ago: [NOW]-3(MINUTES)
.
Value of these fields will be prepare before run the test.
5.4. GroovyScript in datasets
To use Groovy scriptable datasets you need to add the next dependency:
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.6</version> <scope>test</scope> </dependency>
Let’s look at the next document:
@Document
public class Task {
@Id
private String id;
private String title;
private int estimate;
private Date createTime;
}
You can write groovy scripts in the values of fields:
{
"com.antkorwin.springtestmongoexamples.model.Task": [
{
"id": "55f3ed00b1375a48e618300a",
"title": "black magic",
"estimate": "groovy: (1..5).sum()",
"createTime": "groovy: new Date(12345)"
}
]
}
Values of these fields will be evaluated before populate data set in the database. And you can write the next test with expected values of this fields:
@Test
@MongoDataSet(cleanBefore = true, (1)
cleanAfter = true, (2)
value = "dataset/init_task_groovy.json")
void groovyInitTest() {
Task task = taskService.get("55f3ed00b1375a48e618300a");
assertThat(task).extracting(Task::getEstimate, Task::getCreateTime)
.contains(1 + 2 + 3 + 4 + 5, new Date(12345));
}
-
drops all collection before populate a dataset
-
clean database after the test execution
5.5. JavaScript in datasets
Unlike Groovy scripts, Javascript comes by default in JDK and you don’t need some extra dependencies to work with JavaScript.
Let’s consider the next simple document class:
@Document
public class Bar {
@Id
private String id;
private String data;
}
and make the dataset file javascript-dataset.json
:
{
"com.antkorwin.springtestmongo.Bar": [
{
"id": "55f3ed00b1375a48e618300a",
"data": "js: 2+3"
}
]
}
And now we expecting a successful result of this test:
@Test
@MongoDataSet(value = "javascript-dataset.json")
public void jsDataset() throws Exception {
Bar simpleDoc = mongoTemplate.findById("55f3ed00b1375a48e618300a", Bar.class);
Assertions.assertThat(simpleDoc)
.isNotNull()
.extracting(Bar::getData)
.containsOnly(5);
}
5.6. Immutable data set
If you need to assert that data in the mongodb do not change after test execution then you can use parameter readOnly
in the annotation MongoDataSet
as you can see below:
@Test
@MongoDataSet(value = "/dataset/foo-init.json", readOnly = true)
void testImportByMongoDataSetAnnotation() {
Foo fooDoc = mongoTemplate.findById("77f3ed00b1375a0000000001", Foo.class);
// if you change data in MongoDB here, then this test will fail.
}
6. Expecting the state of the database after a test execution
spring-test-mongo
provides you an ability to declare dataset that you expect in MongoDB after test execution.
Let’s try to use it by the testing creation of new Task:
@Test
@ExpectedMongoDataSet("dataset/create_task_expected.json")
void create() {
mongoTemplate.save(new Task("black magic", 123));
}
content of the expected dataset:
{
"com.antkorwin.springtestmongoexamples.model.Task": [
{
"title": "black magic",
"estimate": 123
}
]
}
6.1. Using regular-expressions in datasets
Annotation ExpectedDataSet provide you an ability to use regular expressions in JSON datasets. For example you can describe expected field as: regex: ^data-.$
Let’s look at the dataset of ContactInfo documents:
{
"com.antkorwin.springtestmongoexamples.model.ContactInfo": [
{
"title": "e-mail",
"value": "regex: \\S+@\\S+\\.\\S+$" (1)
}
]
}
-
using regex to check email value from database
6.2. Date & Time matching
It works the same as import dataset but need to use prefix date-match:
instead od date:
in the expected value of fields:
{
"com.antkorwin.springtestmongo.Foo": [
{
"time": "date-match:[NOW]",
"counter" : 1
},
{
"time":"date-match:[NOW]+3(MINUTES)",
"counter" : 2
}
]
}
Now we can run this test:
@Test
@ExpectedMongoDataSet("dataset/expect_with_dates.json")
void dateTimeNow() {
Date now = new Date();
Date plus3min = new Date(now.getTime() + TimeUnit.MINUTES.toMillis(3));
mongoTemplate.save(new Foo(now, 1));
mongoTemplate.save(new Foo(plus3min, 2));
}
Date matching checks time with a threshold value, and you can set amount of this threshold in the date-matcher like this: date-match:[NOW]+25(SECONDS){THR=0}
By default threshold value is 10 000 milliseconds.
You can see syntactic of the match date pattern on the picture above.
6.3. JavaScript matching
You can use javascript in expected datasets:
{
"com.antkorwin.springtestmongo.Foo": [
{
"counter" : 17
},
{
"counter" : "js-match: value % 2 == 0" (1)
}
]
}
-
javascript expression expected even value in database
@Test
@MongoDataSet(cleanBefore = true, cleanAfter = true)
@ExpectedMongoDataSet("js_match.json")
void matchJavaScript() {
mongoTemplate.save(new Bar(17));
mongoTemplate.save(new Bar(32));
}
6.4. Groovy matching
Also, you can use groovy scripts for matching values after a test execution. To work with groovy you need to add it in dependencies (see GroovyScript in datasets section)
{
"com.antkorwin.springtestmongo.Foo": [
{
"counter" : "groovy-match: value == 10+7"
},
{
"counter" : "groovy-match: value == (1..10).sum()"
}
]
}
and now we can run this test:
@Test
@MongoDataSet(cleanBefore = true, cleanAfter = true)
@ExpectedMongoDataSet("js_match.json")
void matchJavaScript() {
mongoTemplate.save(new Bar(17));
mongoTemplate.save(new Bar(55));
}
7. Use GeoJson in data sets
With this library you can use different GeoJson data types in your tests. Let’s consider the next document:
@Document
public class StarShip {
@Id
private String id;
private String name;
private int armor;
private int damage;
private GeoJsonPoint location;
private GeoJsonPolygon shape;
}
7.1. Export GeoJson data after tests execution in the dataset
When you use @ExportMongoDataSet
annotation in tests then documents from the database will be serialized to the target file in a next way:
{ "com.jupiter.tools.spring.test.mongo.documents.StarShip": [ { "id": "5cbbff29921376648d6f4e81", "name": "Dreadnought", "armor": 100, "damage": 50, "location": { "type": "Point", "coordinates": [20.0, 40.0] }, "shape": { "type": "Polygon", "coordinates": [ [ [20.0, 40.0], [22.0, 42.0], [22.0, 40.0], [20.0, 42.0] ] ] } } ] }
example of the test with the exporting dataset:
@Test
@ExportMongoDataSet(outputFile = "target/dataset/export.json")
void exportPolygon() {
Point p1 = new Point(20, 40);
Point p2 = new Point(22, 42);
Point p3 = new Point(22, 40);
Point p4 = new Point(20, 42);
StarShip dreadnought = StarShip.builder()
.name("Dreadnought")
.armor(100)
.damage(50)
.location(new GeoJsonPoint(20, 40))
.shape(new GeoJsonPolygon(p1, p2, p3, p4))
.build();
mongoTemplate.save(dreadnought);
}
7.2. Expected GeoJson in the data set
Let’s look at the sample of the searching an object in MongoDB within the (GeoJson) rectangle boundary:
@Test
@MongoDataSet(value = "dataset/geo/geo_within.json")
void findWithinRectangle() {
// Act
GeoJsonPolygon boundary = new GeoJsonPolygon(new Point(0, 0),
new Point(10, 0),
new Point(10, 10),
new Point(0, 10),
new Point(0, 0));
Query query = new Query();
query.addCriteria(Criteria.where("location")
.within(boundary));
// Act
List<StarShip> ships = mongoTemplate.find(query, StarShip.class);
// Assert
assertThat(ships).hasSize(1);
assertThat(ships.get(0).getName()).isEqualTo("x-wing");
}
we used a next data set with two objects in the file dataset/geo/geo_within.json
:
{
"com.jupiter.tools.spring.test.mongo.documents.StarShip": [
{
"id": "5cbaa745921376602705886f",
"name": "x-wing",
"location": {
"type": "Point",
"coordinates": [
1.0,
5.0
]
}
},
{
"id": "5cbaa74592137660270588ff",
"name": "falcon",
"location": {
"type": "Point",
"coordinates": [
15.0,
10.0
]
}
}
]
}
8. MongoDb transactions support
Since 0.15 version of the spring-test-mongo
supports transactions in mongodb. MongoDB currently supports transactions over a replica set, you can read about this feature here: https://www.baeldung.com/spring-data-mongodb-transactions