GH-4 - Polishing.

Turn embedded MongoDB test setup into proper bean definitions to make sure databases get started and shut down properly with ApplicationContexts. Previously, the database was held in a static variable shut down after a particular test class had been concluded but bean instances held in the cached (and potentially reused ApplicationContext) were still referring to that already shut down database which prevented then from shutting down properly.
This commit is contained in:
Oliver Drotbohm
2022-07-26 18:04:24 +02:00
parent 314a3823ad
commit 9f5d8ec63b
3 changed files with 151 additions and 116 deletions

View File

@@ -15,6 +15,14 @@
*/
package org.springframework.modulith.events.mongodb;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -24,50 +32,56 @@ import org.springframework.modulith.events.CompletableEventPublication;
import org.springframework.modulith.events.EventPublication;
import org.springframework.modulith.events.EventPublicationRepository;
import org.springframework.modulith.events.PublicationTargetIdentifier;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.springframework.util.Assert;
/**
* Repository to store {@link EventPublication}s in a MongoDB.
*
* @author Björn Kieling
* @author Dmitry Belyaev
* @author Oliver Drotbohm
*/
public class MongoDbEventPublicationRepository implements EventPublicationRepository {
class MongoDbEventPublicationRepository implements EventPublicationRepository {
private final MongoTemplate mongoTemplate;
/**
* Creates a new {@link MongoDbEventPublicationRepository} for the given {@link MongoTemplate}.
*
* @param mongoTemplate must not be {@literal null}.
*/
public MongoDbEventPublicationRepository(MongoTemplate mongoTemplate) {
Assert.notNull(mongoTemplate, "MongoTemplate must not be null!");
this.mongoTemplate = mongoTemplate;
}
@Override
public EventPublication create(EventPublication publication) {
mongoTemplate.save(domainToDocument(publication));
return publication;
}
@Override
public EventPublication update(CompletableEventPublication publication) {
findDocumentsByEventAndTargetIdentifier(publication.getEvent(), publication.getTargetIdentifier())
return findDocumentsByEventAndTargetIdentifier(publication.getEvent(), publication.getTargetIdentifier())
.stream()
.findFirst()
.ifPresent(document -> {
document.setCompletionDate(publication.getCompletionDate().orElse(null));
mongoTemplate.save(document);
});
return publication;
.map(document -> document.setCompletionDate(publication.getCompletionDate().orElse(null)))
.map(mongoTemplate::save)
.map(this::documentToDomain)
.orElse(publication);
}
@Override
public List<EventPublication> findIncompletePublications() {
var query = Query.query(Criteria.where("completionDate").isNull());
return mongoTemplate.find(query, MongoDbEventPublication.class).stream() //
.map(this::documentToDomain) //
.collect(Collectors.toList());
@@ -97,10 +111,12 @@ public class MongoDbEventPublicationRepository implements EventPublicationReposi
.where("event").is(eventAsMongoType) //
.and("listenerId").is(targetIdentifier.getValue())) //
.with(Sort.by("publicationDate").ascending());
return mongoTemplate.find(query, MongoDbEventPublication.class);
}
private MongoDbEventPublication domainToDocument(EventPublication publication) {
return new MongoDbEventPublication( //
publication.getPublicationDate(), //
publication.getTargetIdentifier().getValue(), //
@@ -148,5 +164,4 @@ public class MongoDbEventPublicationRepository implements EventPublicationReposi
return this;
}
}
}

View File

@@ -15,21 +15,17 @@
*/
package org.springframework.modulith.events.mongodb;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.within;
import static org.assertj.core.api.Assertions.*;
import lombok.Value;
import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import lombok.Getter;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.modulith.events.CompletableEventPublication;
@@ -38,60 +34,22 @@ import org.springframework.modulith.events.PublicationTargetIdentifier;
import org.springframework.modulith.testapp.TestApplication;
import org.springframework.test.context.ContextConfiguration;
import com.mongodb.client.MongoClients;
import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfig;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.runtime.Network;
import lombok.EqualsAndHashCode;
/**
* @author Björn Kieling
* @author Dmitry Belyaev
*/
@DataMongoTest
@ContextConfiguration(classes = TestApplication.class)
class MongoDbEventPublicationRepositoryTest {
class MongoDbEventPublicationRepositoryTest extends WithEmbeddedMongoDb {
private static final PublicationTargetIdentifier TARGET_IDENTIFIER = PublicationTargetIdentifier.of("listener");
private static final String CONNECTION_STRING = "mongodb://%s:%d";
private static String ip;
private static int port;
private static MongodExecutable mongodExecutable;
@Autowired MongoTemplate mongoTemplate;
private MongoTemplate mongoTemplate;
private MongoDbEventPublicationRepository repository;
@BeforeAll
static void startupMongoDb() throws IOException {
// Refer to https://www.baeldung.com/spring-boot-embedded-mongodb
ip = "localhost";
port = Network.freeServerPort(Network.getLocalHost());
ImmutableMongodConfig mongodConfig = MongodConfig.builder().version(Version.Main.PRODUCTION)
.net(new Net(ip, port, Network.localhostIsIPv6())).build();
MongodStarter starter = MongodStarter.getDefaultInstance();
mongodExecutable = starter.prepare(mongodConfig);
mongodExecutable.start();
}
@AfterAll
static void shutdownMongoDb() {
mongodExecutable.stop();
}
MongoDbEventPublicationRepository repository;
@BeforeEach
void setUp() {
mongoTemplate = new MongoTemplate(MongoClients.create(String.format(CONNECTION_STRING, ip, port)), "test");
repository = new MongoDbEventPublicationRepository(mongoTemplate);
}
@@ -100,17 +58,18 @@ class MongoDbEventPublicationRepositoryTest {
mongoTemplate.remove(MongoDbEventPublication.class).all();
}
@Test
@Test // GH-4
void shouldPersistAndUpdateEventPublication() {
TestEvent testEvent = new TestEvent("abc");
var testEvent = new TestEvent("abc");
CompletableEventPublication publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
var publication = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
// Store publication
repository.create(publication);
List<EventPublication> eventPublications = repository.findIncompletePublications();
var eventPublications = repository.findIncompletePublications();
assertThat(eventPublications).hasSize(1);
assertThat(eventPublications.get(0).getEvent()).isEqualTo(publication.getEvent());
assertThat(eventPublications.get(0).getTargetIdentifier()).isEqualTo(publication.getTargetIdentifier());
@@ -123,84 +82,74 @@ class MongoDbEventPublicationRepositoryTest {
assertThat(repository.findIncompletePublications()).isEmpty();
}
@Test
@Test // GH-4
void shouldUpdateSingleEventPublication() {
TestEvent testEvent1 = new TestEvent("id1");
TestEvent testEvent2 = new TestEvent("id2");
CompletableEventPublication publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
CompletableEventPublication publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
var testEvent1 = new TestEvent("id1");
var testEvent2 = new TestEvent("id2");
var publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
var publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
repository.create(publication1);
repository.create(publication2);
repository.update(publication2.markCompleted());
List<EventPublication> withCompletionDateNull = repository.findIncompletePublications();
assertThat(withCompletionDateNull).hasSize(1);
assertThat(withCompletionDateNull.get(0).getEvent()).isEqualTo(testEvent1);
assertThat(repository.findIncompletePublications()).hasSize(1)
.element(0).extracting(EventPublication::getEvent).isEqualTo(testEvent1);
}
@Nested
class FindByEventAndTargetIdentifier {
@Test
@Test // GH-4
void shouldFindEventPublicationByEventAndTargetIdentifier() {
TestEvent testEvent1 = new TestEvent("abc");
TestEvent testEvent2 = new TestEvent("def");
CompletableEventPublication publication2 = CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER);
repository.create(publication2);
var testEvent1 = new TestEvent("abc");
var testEvent2 = new TestEvent("def");
CompletableEventPublication publication1 = CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER);
repository.create(publication1);
repository.create(CompletableEventPublication.of(testEvent2, TARGET_IDENTIFIER));
repository.create(CompletableEventPublication.of(testEvent1, TARGET_IDENTIFIER));
repository.create(CompletableEventPublication.of(
testEvent1, PublicationTargetIdentifier.of(TARGET_IDENTIFIER.getValue() + "!")));
CompletableEventPublication publication3 = CompletableEventPublication.of(
testEvent1, PublicationTargetIdentifier.of(TARGET_IDENTIFIER.getValue() + "!"));
repository.create(publication3);
var actual = repository.findByEventAndTargetIdentifier(testEvent1, TARGET_IDENTIFIER);
Optional<EventPublication> actual = repository.findByEventAndTargetIdentifier(testEvent1, TARGET_IDENTIFIER);
assertThat(actual).isPresent();
assertThat(actual.get().getEvent()).isEqualTo(testEvent1);
assertThat(actual.get().getTargetIdentifier()).isEqualTo(TARGET_IDENTIFIER);
assertThat(actual).hasValueSatisfying(it -> {
assertThat(it.getEvent()).isEqualTo(testEvent1);
assertThat(it.getTargetIdentifier()).isEqualTo(TARGET_IDENTIFIER);
});
}
@Test
@Test // GH-4
void shouldTolerateEmptyResultTest() {
TestEvent testEvent = new TestEvent("id");
Optional<EventPublication> actual =
repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
var testEvent = new TestEvent("id");
assertThat(actual).isEmpty();
assertThat(repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER)).isEmpty();
}
@Test
@Test // GH-4
void shouldReturnTheOldestEventTest() throws InterruptedException {
TestEvent testEvent = new TestEvent("id");
CompletableEventPublication publicationOld = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
var testEvent = new TestEvent("id");
var publicationOld = repository
.create(CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER));
Thread.sleep(10);
CompletableEventPublication publicationNew = CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER);
repository.create(CompletableEventPublication.of(testEvent, TARGET_IDENTIFIER));
repository.create(publicationNew);
repository.create(publicationOld);
var actual = repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
Optional<EventPublication> actual =
repository.findByEventAndTargetIdentifier(testEvent, TARGET_IDENTIFIER);
assertThat(actual).isNotEmpty();
assertThat(actual.get().getPublicationDate())
.isCloseTo(publicationOld.getPublicationDate(), within(1, ChronoUnit.MILLIS));
assertThat(actual).hasValueSatisfying(it -> {
assertThat(it.getPublicationDate()) //
.isCloseTo(publicationOld.getPublicationDate(), within(1, ChronoUnit.MILLIS));
});
}
}
@EqualsAndHashCode
@Getter
@Value
private static final class TestEvent {
private final String eventId;
private TestEvent(String eventId) {
this.eventId = eventId;
}
String eventId;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith.events.mongodb;
import de.flapdoodle.embed.mongo.MongodProcess;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfig;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.runtime.Network;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
/**
* Setup of embedded MongoDB as Spring beans so that it will properly bound to and shut down with an
* {@link ApplicationContext}.
*
* @author Oliver Drotbohm
*/
@ContextConfiguration(classes = WithEmbeddedMongoDb.TestConfiguration.class)
abstract class WithEmbeddedMongoDb {
@Configuration
static class TestConfiguration {
@Bean
Net mongoDbConfig() throws Exception {
return new Net("localhost", Network.freeServerPort(Network.getLocalHost()), false);
}
@Bean(destroyMethod = "stop")
MongodProcess mongoDbProcess(Net config) throws Exception {
ImmutableMongodConfig mongodConfig = MongodConfig.builder()
.version(Version.Main.PRODUCTION)
.net(config)
.build();
return MongodStarter.getDefaultInstance()
.prepare(mongodConfig)
.start();
}
@Bean
// Artificially depend on MongodProcess so that it will not shut down prior
// to the repository -> template -> client -> process shutdown chain
MongoClient mongoDbClient(Net config, MongodProcess process) {
return MongoClients.create("mongodb://" + config.getBindIp() + ":" + config.getPort());
}
}
}