on
Testing a Spring Data Mongo repository with embedded Mongo on a custom port
Background:
Since version 1.4, Spring Boot introduced the concept of test slices. Spring context can be costly to bootstrap with every test, especially in large applications. Test slices allows Spring to cherry pick the useful parts for a particular test. Spring provides the possibily to create custom slices annotations, but there are also several ready made test slices annotations provided by spring-boot-starter-test
like @WebMvcTest
which allows to test the web layer only (http calls), @DataJpaTest
which bootstraps the jpa repositories only, @DataMongoTest
which bootraps Spring data mongo repositories and launches the de.flapdoodle.embed.mongo embedded mongo database (should be on the test classpath), and many others. While @DataMongoTest
provides an auto-configuration for Mongo related beans and repositories, it’s also possible to customize the configurations such as the port on which Mongo runs. This post shows an example.
Testing a Mongo Repository:
Let’s suppose we want to test the new query method that we added to our Mongo repository:
public interface TransactionRepository extends MongoRepository<Transaction, String> {
Page<Transaction> findBySuccessIsTrueAndCreatedLessThanEqualAndUserIdOrderByCreatedDesc(long created, String userId, Pageable pageRequest);
}
With Transaction
being:
@Document
public class Transaction {
@Id
String id;
@Indexed
boolean success;
@Indexed
String userId;
@Indexed
long created;
//constructors, getters and setters..etc
}
Our test looks like:
@ExtendWith(SpringExtension.class)
@DataMongoTest
public class TransactionRepositoryTest {
@Autowired
private TransactionRepository transactionRepository;
private final static List<String> USER_ID_LIST = Arrays.asList("b2b1f340-cba2-11e8-ad5d-873445c542a2", "bd5dd3a4-cba2-11e8-9594-3356a2e7ef10");
private static final Random RANDOM = new Random();
@BeforeEach
public void dataSetup() {
Transaction transaction;
for (int i = 0; i < 10; i++) {
String requestId = UUID.randomUUID().toString();
if (i % 2 == 0) {
transaction = new Transaction(requestId, true, USER_ID_LIST.get(RANDOM.nextInt(2)), System.currentTimeMillis());
} else {
transaction = new Transaction(requestId, false, USER_ID_LIST.get(RANDOM.nextInt(2)), System.currentTimeMillis());
}
transactionRepository.save(transaction);
}
}
@Test
public void findSuccessfullOperationsForUserWithCreatedDateLessThanNowTest() {
long now = System.currentTimeMillis();
String userId = USER_ID_LIST.get(RANDOM.nextInt(2));
List<Transaction> resultsPage = transactionRepository.findBySuccessIsTrueAndCreatedLessThanEqualAndUserIdOrderByCreatedDesc(now, userId, PageRequest.of(0, 5)).getContent();
assertThat(resultsPage).isNotEmpty();
assertThat(resultsPage).extracting("userId").allMatch(id -> Objects.equals(id, userId));
assertThat(resultsPage).extracting("created").isSortedAccordingTo(Collections.reverseOrder());
assertThat(resultsPage).extracting("created").first().matches(createdTimeStamp -> (Long)createdTimeStamp <= now);
assertThat(resultsPage).extracting("success").allMatch(sucessfull -> (Boolean)sucessfull == true);
}
}
Customzing the config:
Everything is ok so far. Now, suppose we want to customize our mongo configuration e.g the port from which mongo is launched. We have to exclude the Mongo auto-configuration first:
@DataMongoTest(excludeAutoConfiguration= {EmbeddedMongoAutoConfiguration.class})
And then we can provide our own custom configuration:
@Configuration
static class MongoConfiguration implements InitializingBean, DisposableBean {
MongodExecutable executable;
@Override
public void afterPropertiesSet() throws Exception {
String host = "localhost";
int port = 27019;
IMongodConfig mongodConfig = new MongodConfigBuilder().version(Version.Main.PRODUCTION)
.net(new Net(host, port, Network.localhostIsIPv6()))
.build();
MongodStarter starter = MongodStarter.getDefaultInstance();
executable = starter.prepare(mongodConfig);
executable.start();
}
@Bean
public MongoDbFactory factory() {
// also possible to connect to a remote or real MongoDB instance
MongoDbFactory mongoDbFactory = new SimpleMongoDbFactory(new MongoClientURI("mongodb://localhost:27019/test_db"));
return mongoDbFactory;
}
@Bean
public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory) {
MongoTemplate template = new MongoTemplate(mongoDbFactory);
template.setWriteConcern(WriteConcern.ACKNOWLEDGED);
return template;
}
@Bean
public MongoRepositoryFactoryBean mongoFactoryRepositoryBean(MongoTemplate template) {
MongoRepositoryFactoryBean mongoDbFactoryBean = new MongoRepositoryFactoryBean(TransactionRepository.class);
mongoDbFactoryBean.setMongoOperations(template);
return mongoDbFactoryBean;
}
@Override
public void destroy() throws Exception {
executable.stop();
}
}
The MongoDbFactory
, MongoTemplate
, MongoRepositoryFactoryBean
are the building blocks of Spring data configuration for MongoDB. Additionally, we have extended InitializingBean
, DisposableBean
classes for convenience to start/stop the Embedded Mongo instance after the MongoConfiguration
bean creation/disposal. It is also possible to launch/stop the instance in methods annotated with @BeforeAll
, @AfterAll
JUnit annotations in the main test class.
Now, our Mongo will launched at port 27019.
The full example can be found here: https://github.com/zak905/mongo-spring-test-demo
Update (18-03-2022)
As of the version 2.6 of Spring boot, the version of de.flapdoodle.embed.mongo have been upgraded, which introduces a number of changes to the embeded Mongo APIs:
The mongo config should look like:
@Configuration
static class MongoConfiguration {
@Bean
public MongoDatabaseFactory factory() {
return new SimpleMongoClientDatabaseFactory("mongodb://localhost:27019/imager200_test");
}
@Bean
public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory) {
MongoTemplate template = new MongoTemplate(mongoDbFactory);
template.setWriteConcern(WriteConcern.ACKNOWLEDGED);
return template;
}
@Bean
public MongoRepositoryFactoryBean mongoFactoryRepositoryBean(MongoTemplate template) {
MongoRepositoryFactoryBean mongoDbFactoryBean = new MongoRepositoryFactoryBean(TransactionRepository.class);
mongoDbFactoryBean.setMongoOperations(template);
return mongoDbFactoryBean;
}
}
The demo repo has been updated as well.