You can achieve some amazing results with Spring Boot Test for your Integration Testing. I’ll be covering how to execute different groups (sub-sets) of Integration Tests which cover different layers (or slices) of your application. Applying this layered approach will give you increased flexibility and the opportunity to identity problem areas earlier on.
YouTube Tutorial Instead?
If you want to view the Video Tutorial version of this Post instead, then click below. It covers everything in this Post and more.
Spring Boot Integration Test Slices
Spring Boot Test (1.5.0) now offers a convenient way to run an Integration Test against an isolated layer or “slice” of our application. This is great since it gives us the opportunity to run a scaled down test which can pin-point problem areas early on before we write more encompassing Integration Tests. Once those tests pass, we can move up the application stack with more confidence!
We’ll be running a MongoDB Slice Integration Test (S.I.T ; I just made that up!) via @DataMongoTest which will ensure we can …
- Correctly load our Spring ApplicationContext
- Inject a MongoTemplate to confirm a proper connection can be established (amongst other things)
- Scan our @Document entity classes
- Use our @Repository Classes to ensure correct mappings and Queries.
The @…Test annotations (see Spring Boot Reference Guide for full list) are all used for specific slice testing which are supported through the magic of auto-configuration, thus getting your test setup quickly.
Basically, this asserts that the Spring Data MongoDB components are correctly configured and functioning properly. The auto-configuration will handle setting up an embedded MongoDB instance for you if you have the following dependency available in your pom.xml
1 2 3 4 5 |
<dependency> <groupId>de.flapdoodle.embed</groupId> <artifactId>de.flapdoodle.embed.mongo</artifactId> <scope>test</scope> </dependency> |
The MongoDB Integration Test Slice
The following Test Class asserts that our MongoDB Slice is working properly. Again, this is all possible via the @DataMongoTest annotation.
You can get this source code and more at MVP Java’s GitHub Account
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
@IfProfileValue(name = ACTIVE_PROFILES_PROPERTY_NAME, value = "it-embedded") @RunWith(SpringRunner.class) @DataMongoTest public class MongoSliceIT { String collectionName; LogRecord logRecToInsert; @Autowired private MongoTemplate mongoTemplate; @Autowired private LogRepository logRepository; @Before public void before() { collectionName = "logs"; logRecToInsert = new LogRecord("INFO", "Test Info Log messge"); } @After public void after() { mongoTemplate.dropCollection(collectionName); } @Test public void checkMongoTemplate() { assertNotNull(mongoTemplate); DBCollection createdCollection = mongoTemplate.createCollection(collectionName); assertTrue(mongoTemplate.collectionExists(collectionName)); } @Test public void checkDocumentAndQuery() { mongoTemplate.save(logRecToInsert, collectionName); Query query = new Query(new Criteria() .andOperator(Criteria.where("level").regex(logRecToInsert.getLevel()), Criteria.where("message").regex(logRecToInsert.getMessage()))); LogRecord retrievedLogRecord = mongoTemplate.findOne(query, LogRecord.class, collectionName); assertNotNull(retrievedLogRecord); } @Test public void checkLogRepository() { assertNotNull(logRepository); LogRecord savedLogRecord = logRepository.save(logRecToInsert); assertNotNull(logRepository.findOne(savedLogRecord.getId())); } } |
The Integration Test above gets executed if the “it-embedded” Spring Profile has been specified via System Properly. This is verified by the @IfProfileValue annotation which is wired to check the System Property “spring.profiles.active” via its ProfileValueSource implementation; named SystemProfileValueSource. If it hasn’t been activated, then the test is skipped.
Notice that there are no Services, Controllers, Web Server involved here; that’s the point! If that slice doesn’t work, what’s the point of moving on? In any event, @DataMongoTest does not support that by default (more on that below).
Moving up the Application Stack
We can perform wider reaching Integration Tests, such as on a real MongoDB instance running locally or include an embedded Tomcat web server. Maybe you want an additional @Service, @Component or @Controller class?
If you want to widen the @DataMongoTest slice test to include a @Service class then simply add it into the include filter like so ..
1 |
@DataMongoTest(includeFilters = @Filter(Service.class)) |
Now you’re able to dependency inject a @Service into your Integration Test.
Going All Out with @SpringBootTest
Now that we’re felling confident with the passing Slice Integration Tests, we can go even wider now. We’ll start using the @SpringBootTest annotation which is like a regular Spring TestContext but is more geared towards a Spring Boot environment in order to get your ApplicationContext loaded for your Integration Tests.
It also packs a nice punch since we can use its webEnvironment attribute to load either a mocked or real servlet container environment (or NONE, if that’s what you want). It even provides us with the opportunity to dependency inject a TestRestTemplate into our tests, nice!
Here’s an example with a real embedded Tomcat Servlet Container running on a random port (reduces potential port conflicts) and an embedded MongoDB instance running.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@IfProfileValue(name = ACTIVE_PROFILES_PROPERTY_NAME, value = "it-embedded") @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @EnableAutoConfiguration public class MongoTomcatIT { @Autowired private TestRestTemplate restTemplate; @Test public void ensureLoggingWorks() { LogRecord logRecord = new LogRecord("INFO", "This is a test"); ResponseEntity responseEntity = restTemplate.postForEntity( "/log", logRecord, LogRecord.class); LogRecord logRecordReturned = responseEntity.getBody(); assertNotNull("Should have an PK", logRecordReturned.getId()); } } |
@SpringBootTest is not part of the spring-boot-test-autoconfigure project, therefore it has no knowledge of auto-configuration. This is why you don’t see it used with @DataMongoTest, which is all about auto-configuration and slices.
By adding the @EnableAutoConfiguration we are allowing the embedded MongoDB instance to be used as we did before. Again, this test only executes if the “it-embedded” Spring Profile was activated via System Property.
Integration Test with a Real MongoDB Instance
Let’s say we also wanted to test another layer, this time with a locally available MongoDB instance running on your developer machine (not embedded). This IntegrationTest would not rely on the auto-configuration magic, so remove the @EnableAutoConfiguration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@IfProfileValue(name = ACTIVE_PROFILES_PROPERTY_NAME, value = "it") @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class RealMongoEmbeddTomcatIT { @Autowired private TestRestTemplate restTemplate; @Test public void ensureLoggingWorks() { LogRecord logRecord = new LogRecord("INFO", "This is a test on real monngoDB"); ResponseEntity responseEntity = restTemplate.postForEntity( "/log", logRecord, LogRecord.class); LogRecord logRecordReturned = responseEntity.getBody(); assertNotNull("Should have an PK", logRecordReturned.getId()); } } |
This time we are only executing the test if Spring Profile “it” has been set (signifying a non-embedded mongoDB Integration Test)
Now we have 2 different types of Integration Tests.
- Two Tests with Spring Profile “it-embedded“
- One Test with Spring Profile “it“
How do we go About Selectively Executing These Tests?
To achieve that flexibility, we need to do the following 3 things …
First you need to setup the mongoDB configuration properties so that they can be read by Spring. The embedded and real mongoDB instances will surely have different IP’s right !
Setup all these MongoDB configuration properties in separate property files which follow the naming convention application-{$profile}.properties This allows Spring to pick up the correct file based on the correct profile activated by you (I’ll show you how to do that in a minute).
For our example, we will have 2 files …
1 2 3 4 |
application-it.properties application-it-embedded.properties ** Obviously you would have more in a real app ** |
Below is a small example of the application-it.properties file. The spring.autoconfigure.exclude key is specified in order to stop loading the flapdoodle embedded mongoDB. Since its an available test dependency. Spring Boot tries to auto-configure it for you too but we don’t need it. We simply say, no thanks (exclude)!
1 2 3 4 |
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration mongodb.database=testDB mongodb.host=localhost mongodb.port=27017 |
Secondly, We’ll create a @Configuration Class for Spring Boot to pick up in order to properly build our MongoTemplate. Notice the @Profile (“!it-embedded). That means associate the following Spring @Beans to every Spring Profile but NOT (!) the profile named “it-embedded”. If you don’t do this, you’ll get errors due to auto-configuration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
@Configuration @Profile("!it-embedded") //or else problems when using @EnableAutoConfiguration public class MongoConfigProfiles { private final MongoProperties mongoProperties; public MongoConfigProfiles(MongoProperties mongoProperties) { this.mongoProperties = mongoProperties; } @Bean public MongoDbFactory mongoDbFactory() throws Exception { MongoClient mongoClient = new MongoClient(mongoProperties.getHost(), mongoProperties.getPort()); return new SimpleMongoDbFactory(mongoClient, mongoProperties.getDatabase()); } @Bean public MongoTemplate mongoTemplate() throws Exception { MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory()); return mongoTemplate; } } @Component public class MongoProperties { private final Environment environment; @Autowired public MongoProperties(Environment environment) { this.environment = environment; } public String getDatabase() { return environment.getProperty("mongodb.database"); } public String getHost() { return environment.getProperty("mongodb.host"); } public int getPort() { return environment.getProperty("mongodb.port", Integer.class); } } |
The great thing about the @Configuration Class above is that Spring Boot will create the MongoDbFactory and MongoTemplate using the correct application-{$profile}.properties file based on the active Spring profile specified via System Property. That means, we don’t need to duplicate code and add different @Profile (“…”) annotations or use numerous @Value annotations to extract property values.
Lastly, specify the active Spring profile from your project root directory on the CLI (or IDE) …
1 2 3 |
mvn clean verify -Dspring.profiles.active=it -OR- mvn clean verify -Dspring.profiles.active=it-embedded |
Once that’s done, it’s up to @IfProfileValue to do its job and you’ll see the correct sub-set of Integration Tests execute. You’re now ready to slice and dice 🙂
Extra extra, read all about it! Get AI (Artificial Intelligence) to write all you Spring Integration tests for you! Check out my post and YouTube video.