The module sda-commons-web-testing
provides a few helpers to support integration tests related to the features provided by SDA Spring
Boot Commons and propagated patterns of SDA SE.
The default configuration of Spring's Actuator with this library is a separate management port.
In tests, this causes some issues that
need configuration.
@SpringBootTest's need to be marked with @DirtiesContext or define management.server.port=0
either in @SpringBootTest(properties = "…") or in src/test/resources/application.properties.
The security concept of SDA SE has a strict separation of authentication
and authorization.
The identity of a user is verified in the application by validating a JWT from an OIDC provider.
The authorization is delegated to an Open Policy Agent sidecar with policies for an actual
environment.
The service implementation grants access based on constraints received from the policy, not on roles
which may be different in each environment or not even available.
sda-commons-web-testing provides ApplicationContextInitializers for integration test contexts to
work with authenticated users and mocked authorization decisions, including constraints as well as
for disabling the security mechanism.
Mocking Authentication and Authorization Constraints
Usually constraints are handled in @Controllers who call the allowed business functions with
applicable parts of the constraints model.
Integration tests with HTTP calls may not be the best solution to test complex logic on constraints.
In such cases the constraints model can be mocked.
Mocking Constraints for Unit Tests Compared to Integration Test
Generating and Validating up-to-date Documentation¶
At SDA SE it is common to publish generated documentation like OpenAPI or AsyncAPI in the repository.
From there it's picked up by our developer portal based on Backstage.
sda-commons-web-testing provides GoldenFileAssertions to validate in a test that such
documentation is up-to-date and updates it when run locally.
It is recommended to test with an embedded MongoDB using Flapdoodle's Spring Boot module
de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring3xand the SDA Spring Boot Commons testing
module.
The version is managed by the library as described above.
Flapdoodle will start a MongoDB server and configures the connection for Spring Boot tests.
The MongoDB version can be selected with (test) application properties:
1
de.flapdoodle.mongodb.embedded.version=4.4.1
To skip Flapdoodle's autoconfiguration and use a provided database (e.g. an AWS DocumentDB), the
property test.mongodb.connection.string (or environment variable TEST_MONGODB_CONNECTION_STRING)
can be used to provide a complete MongoDB Connection String.
This feature is provided by the SDA Spring Boot Commons testing module and is only activated when
Flapdoodle's autoconfiguration is available for the test setup.
Clean up of the collections is important, especially when using a provided database, that may be
reused in a subsequent build.
To ensure backwards compatibility of the database after upgrades, refactorings or changing the
database framework, asserting with a bare MongoClient removes influence of mappers and converters.
Since version 7, the embedded MongoDB instance used for testing has been observed to exhibit
increased memory consumption, which may lead to slower test execution, higher resource usage, or
even out-of-memory errors in environments with limited resources.
This is typically due to each test case starting its own isolated MongoDB instance, which increases
memory overhead and degrades performance.
To mitigate this issue, it is possible to use a shared embedded MongoDB instance that runs once for
all integration tests in your suite. This approach significantly reduces memory overhead and
improves test execution speed.
importde.flapdoodle.embed.mongo.config.Net;importde.flapdoodle.embed.mongo.distribution.Version;importde.flapdoodle.embed.mongo.transitions.Mongod;importde.flapdoodle.embed.mongo.transitions.RunningMongodProcess;importde.flapdoodle.reverse.TransitionWalker;importde.flapdoodle.reverse.transitions.Start;importjava.util.concurrent.atomic.AtomicBoolean;importorg.junit.jupiter.api.extension.BeforeAllCallback;importorg.junit.jupiter.api.extension.ExtensionContext;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;publicclassSharedMongoExtensionimplementsBeforeAllCallback{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(SharedMongoExtension.class);privatestaticTransitionWalker.ReachedState<RunningMongodProcess>running;privatestaticfinalAtomicBooleanstarted=newAtomicBoolean(false);privatestaticfinalObjectmutex=newObject();publicstaticvoidstop(){if(running!=null){running.close();}}@OverridepublicvoidbeforeAll(ExtensionContextcontext){synchronized(mutex){try{if(started.compareAndSet(false,true)){intport=de.flapdoodle.net.Net.freeServerPort();Mongodmongod=Mongod.builder().net(Start.to(Net.class).initializedWith(Net.of("127.0.0.1",port,false))).build();running=mongod.start(Version.Main.V8_0);LOGGER.info("Started embedded MongodDB on Port {}",port);System.setProperty("spring.mongodb.uri","mongodb://127.0.0.1:"+port+"/testdb");}}catch(Exceptione){thrownewRuntimeException("Could not start embedded MongoDB",e);}}}}
To use this extension it is necessary to register it in the test class with:
1234
@ExtendWith(SharedMongoExtension.class)publicclassMyIntegrationTest{// Your tests will now share the same MongoDB instance}
To prevent Spring from starting its own embedded MongoDB instance, add the following to the
application.properties or as a JVM argument:
It is recommended to test with an embedded Kafka using the Spring Boot module
org.springframework.kafka:spring-kafka-test and the SDA Spring Boot Commons testing module.
The version is managed by the library as described above.
import staticorg.awaitility.Awaitility.await;import staticorg.mockito.Mockito.verify;importjava.util.Map;importorg.junit.jupiter.api.Test;importorg.sdase.commons.spring.boot.kafka.test.KafkaTestApp;importorg.sdase.commons.spring.boot.kafka.test.SomeService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.kafka.core.KafkaTemplate;importorg.springframework.kafka.test.context.EmbeddedKafka;importorg.springframework.test.context.bean.override.mockito.MockitoSpyBean;@SpringBootTest(classes=KafkaTestApp.class,webEnvironment=SpringBootTest.WebEnvironment.MOCK,properties={"management.server.port=0"})@EmbeddedKafkaclassKafkaConsumerIntegrationTest{@AutowiredKafkaTemplate<String,Object>kafkaTemplate;@MockitoSpyBeanSomeServicesomeService;@Value("${app.kafka.consumer.topic}")// same as configured for the consumerprivateStringtopic;@TestvoidshouldDoSomethingWithMessage(){StringgivenMessageKey="some-key";vargivenMessageValue=Map.of("property","value");kafkaTemplate.send(topic,givenMessageKey,givenMessageValue);// verify that a repository saved data derived from the message, an external service is// called or whatever should happen when the message is receivedawait().untilAsserted(()->verify(someService).didTheJob(givenMessageValue));}}
import staticorg.assertj.core.api.Assertions.assertThat;importjava.time.Duration;importjava.util.Set;importjava.util.concurrent.ExecutionException;importjava.util.concurrent.TimeoutException;importorg.apache.kafka.clients.consumer.ConsumerRecord;importorg.apache.kafka.clients.consumer.ConsumerRecords;importorg.apache.kafka.clients.consumer.KafkaConsumer;importorg.apache.kafka.common.serialization.StringDeserializer;importorg.junit.jupiter.api.Test;importorg.sdase.commons.spring.boot.kafka.test.KafkaTestApp;importorg.sdase.commons.spring.boot.kafka.test.KafkaTestModel;importorg.sdase.commons.spring.boot.kafka.test.KafkaTestProducer;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.kafka.test.EmbeddedKafkaBroker;importorg.springframework.kafka.test.context.EmbeddedKafka;importorg.springframework.kafka.test.utils.KafkaTestUtils;@SpringBootTest(classes=KafkaTestApp.class,webEnvironment=SpringBootTest.WebEnvironment.MOCK,properties={"management.server.port=0"})@EmbeddedKafkaclassKafkaProducerIntegrationTest{@AutowiredEmbeddedKafkaBrokerkafkaEmbedded;@AutowiredKafkaTestProducerkafkaTestProducer;@Value("${app.kafka.producer.topic}")// as defined for the producerStringtopic;@TestvoidshouldProduceMessage()throwsExecutionException,InterruptedException,TimeoutException{KafkaTestModelgiven=newKafkaTestModel().setCheckInt(1).setCheckString("Hello World!");kafkaTestProducer.send(given);varactualRecords=consumeRecords(topic);assertThat(actualRecords).hasSize(1).first().extracting(ConsumerRecord::value).asString().contains("Hello World!");}ConsumerRecords<String,String>consumeRecords(Stringtopic){try(vartestConsumer=createTestConsumer(topic)){returntestConsumer.poll(Duration.ofSeconds(10));}}KafkaConsumer<String,String>createTestConsumer(Stringtopic){KafkaConsumer<String,String>consumer=newKafkaConsumer<>(KafkaTestUtils.consumerProps(kafkaEmbedded.getBrokersAsString(),"test-consumer",true),newStringDeserializer(),newStringDeserializer());consumer.subscribe(Set.of(topic));returnconsumer;}}
Since version 7, performance issues with the embedded Kafka for testing have been observed. This is typically due to each test case starting its own isolated Kafka instance, which increases memory overhead and degrades performance.
To mitigate this issue, it is possible to configure a shared embedded Kafka broker that runs once for the entire test suite. This approach significantly reduces memory overhead and improves test execution speed.
To achieve this:
Remove the @EmbeddedKafka annotation from the test classes if necessary, as it will now be replaced by Gradle configuration.
Configure the embedded Kafka broker via build.gradle, using system properties to control its behavior.
Update the test block in the build.gradle with the following system properties to enable a shared embedded Kafka broker:
12345
test{systemProperty("spring.kafka.global.embedded.enabled","true")systemProperty("spring.kafka.embedded.ports","0")// Use a single random portsystemProperty("spring.kafka.embedded.partitions","1")}
While this setup improves performance, direct access to the EmbeddedKafkaBroker class is lost, which provides useful features like:
The ability to reset partitions or clear data between tests
This means that advanced configuration and debugging must be managed outside the EmbeddedKafkaBroker class, which can pose a limitation for complex test scenarios.
sda-commons-web-testing provides the annotation S3Test to start a local S3 mock with
Robothy local-s3 and configure Spring Boot as needed for
sda-commons-starter-s3.
@S3Test must be placed before @SpringBootTest if used for a full integration test with an
application context.
The dependency io.github.robothy:local-s3-rest must be added as test dependency to the project.
software.amazon.awssdk:s3 is needed as well and comes with io.awspring.cloud:spring-cloud-aws-s3
via sda-commons-starter-s3.
The versions are managed by the library as described above.
S3Test can also be used in tests without a Spring Context.
Test, set up and tear down methods can request S3Client and LocalS3Configuration as
method parameter to interact with the S3 mock or set up the tested services.