From 9f5d8ec63b03339c5107359be24d99c0c4d3af66 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Tue, 26 Jul 2022 18:04:24 +0200 Subject: [PATCH] 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. --- .../MongoDbEventPublicationRepository.java | 47 ++++-- ...MongoDbEventPublicationRepositoryTest.java | 149 ++++++------------ .../events/mongodb/WithEmbeddedMongoDb.java | 71 +++++++++ 3 files changed, 151 insertions(+), 116 deletions(-) create mode 100644 spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/WithEmbeddedMongoDb.java diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java index 30572160..7b5efb34 100644 --- a/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java +++ b/spring-modulith-events/spring-modulith-events-mongodb/src/main/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepository.java @@ -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 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; } } - } diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java index 32e69bd6..0f143e11 100644 --- a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java +++ b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/MongoDbEventPublicationRepositoryTest.java @@ -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 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 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 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 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 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; } } diff --git a/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/WithEmbeddedMongoDb.java b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/WithEmbeddedMongoDb.java new file mode 100644 index 00000000..d9bedff5 --- /dev/null +++ b/spring-modulith-events/spring-modulith-events-mongodb/src/test/java/org/springframework/modulith/events/mongodb/WithEmbeddedMongoDb.java @@ -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()); + } + } +}