Skip to content

Testing

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.

Test Setup

Dependencies

1
2
3
4
5
6
7
8
dependencies {
  implementation enforcedPlatform("org.sdase.commons.spring.boot:sda-commons-dependencies:$sdaSpringCommonsVersion")
  implementation enforcedPlatform("org.sdase.commons.spring.boot:sda-commons-bom:$sdaSpringCommonsVersion")

  implementation 'org.sdase.commons.spring.boot:sda-commons-starter-web'

  testImplementation 'org.sdase.commons.spring.boot:sda-commons-web-testing'
}

Management Port

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.

Provided Libraries

sda-commons-web-test comes with managed and aligned transitive dependencies, that can be used without adding them to the dependency tree, including:

  • org.springframework.boot:spring-boot-starter-test
  • org.springframework.cloud:spring-cloud-contract-wiremock
  • org.junit.jupiter:junit-jupiter
  • org.assertj:assertj-core
  • com.jayway.jsonpath:json-path
  • org.awaitility:awaitility
  • org.mockito:mockito-junit-jupiter

Authentication and Authorization

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
 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
52
53
54
55
56
57
58
59
60
61
import static org.assertj.core.api.Assertions.assertThat;

import jakarta.ws.rs.HttpMethod;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.testing.auth.AuthMock;
import org.sdase.commons.spring.boot.web.testing.auth.EnableSdaAuthMockInitializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = "management.server.port=0")
@ContextConfiguration(initializers = {EnableSdaAuthMockInitializer.class})
class AuthTest {
  @LocalServerPort private int port;

  @Autowired AuthMock authMock;

  @BeforeEach
  void reset() {
    authMock.reset();
  }

  @Test
  void shouldRejectRequest() {
    authMock.authorizeRequest().withHttpMethod(HttpMethod.GET).withPath("/cars").deny();

    var response =
        authMock.authentication().authenticatedClient().getForEntity(carsApiUrl(), Cars.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
  }

  @Test
  void shouldAllowWithConstraints() {
    authMock
        .authorizeRequest()
        .withHttpMethod(HttpMethod.GET)
        .withPath("/cars")
        .allowWithConstraint(Map.of("drivers", List.of("driver-123")));

    var response =
        authMock
            .authentication()
            .withSubject("driver-123")
            .authenticatedClient()
            .getForEntity(carsApiUrl(), Cars.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
  }

  private String carsApiUrl() {
    return "http://localhost:%s/api/cars".formatted(port);
  }
}
Disable Authentication and Authorization
 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
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.testing.auth.DisableSdaAuthInitializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = "management.server.port=0")
@ContextConfiguration(initializers = {DisableSdaAuthInitializer.class})
class DisabledAuthTest {
  @LocalServerPort private int port;

  @Autowired TestRestTemplate restTemplate;

  @Test
  void shouldGetResourceWithoutAuthentication() {
    var response = restTemplate.getForEntity(carsApiUrl(), Cars.class);

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
  }

  private String carsApiUrl() {
    return "http://localhost:%s/api/cars".formatted(port);
  }
}

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
 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
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.app.constraints.test.SomeConstraints;
import org.sdase.commons.spring.boot.web.app.constraints.test.SomeController;
import org.sdase.commons.spring.boot.web.app.constraints.test.SomeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

@SpringBootTest(classes = SomeController.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class SomeControllerTest {

  @MockBean SomeConstraints mockConstraints;
  @MockBean SomeService someService;

  @Autowired SomeController constraintsAwareController;

  @Test
  void shouldBeAdmin() {
    when(mockConstraints.isAdmin()).thenReturn(true);
    constraintsAwareController.getUserCategory();
    verify(someService, times(1)).doAsAdmin();
    verifyNoMoreInteractions(someService);
  }

  @Test
  void shouldBeUser() {
    when(mockConstraints.isAdmin()).thenReturn(false);
    constraintsAwareController.getUserCategory();
    verify(someService, times(1)).doAsUser();
    verifyNoMoreInteractions(someService);
  }
}
 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
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.app.constraints.test.App;
import org.sdase.commons.spring.boot.web.testing.auth.AuthMock;
import org.sdase.commons.spring.boot.web.testing.auth.EnableSdaAuthMockInitializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ContextConfiguration;

@SpringBootTest(
    classes = App.class,
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = "management.server.port=0")
@ContextConfiguration(initializers = {EnableSdaAuthMockInitializer.class})
class SomeControllerIntegrationTest {

  @LocalServerPort private int port;

  @Autowired AuthMock authMock;

  @Test
  void shouldBeAdmin() {
    authMock.authorizeAnyRequest().allowWithConstraint(Map.of("admin", true));
    var actual =
        authMock.authentication().authenticatedClient().getForEntity(serviceUrl(), String.class);
    assertThat(actual.getBody()).isEqualTo("admin");
  }

  @Test
  void shouldBeUser() {
    authMock.authorizeAnyRequest().allowWithConstraint(Map.of("admin", false));
    var actual =
        authMock.authentication().authenticatedClient().getForEntity(serviceUrl(), String.class);
    assertThat(actual.getBody()).isEqualTo("user");
  }

  private String serviceUrl() {
    return "http://localhost:%s/api/me/category".formatted(port);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.sdase.commons.spring.boot.web.auth.opa.AbstractConstraints;
import org.sdase.commons.spring.boot.web.auth.opa.Constraints;

@Constraints
public class SomeConstraints extends AbstractConstraints {
  private boolean admin;

  public boolean isAdmin() {
    return admin;
  }

  public SomeConstraints setAdmin(boolean admin) {
    this.admin = admin;
    return this;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SomeController {

  private final SomeConstraints constraints;
  private final SomeService someService;

  public SomeController(SomeConstraints constraints, SomeService someService) {
    this.constraints = constraints;
    this.someService = someService;
  }

  @GetMapping("/me/category")
  public String getUserCategory() {
    return constraints.isAdmin() ? someService.doAsAdmin() : someService.doAsUser();
  }
}

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.

Generating and updating OpenAPI
 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
import java.io.IOException;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.testing.GoldenFileAssertions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {
      "springdoc.packagesToScan=org.sdase.commons.spring.boot.web.app.example",
      "management.server.port=0"
    })
class OpenApiDocumentationTest {

  @LocalServerPort private int port;

  @Autowired private TestRestTemplate client;

  @Test
  void shouldHaveSameOpenApiInRepository() throws IOException {
    var expectedOpenApi =
        client.getForObject(
            String.format("http://localhost:%s/api/openapi.yaml", port), String.class);

    var actualOpenApiInRepository = Paths.get("openapi.yaml").toAbsolutePath();

    GoldenFileAssertions.assertThat(actualOpenApiInRepository)
        .hasYamlContentAndUpdateGolden(expectedOpenApi);
  }
}

MongoDB

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.

Testing a MongoDB Repository
 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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import static org.assertj.core.api.Assertions.assertThat;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoDatabase;
import java.time.ZonedDateTime;
import java.util.Optional;
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.mongodb.test.TestEntity;
import org.sdase.commons.spring.boot.mongodb.test.TestEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

@SpringBootTest(classes = MongoTestApp.class, webEnvironment = WebEnvironment.MOCK)
class TestEntityRepositoryTest {

  @Autowired MongoClient mongoClient;
  @Autowired MongoConnectionDetails mongoConnectionDetails;

  @Autowired TestEntityRepository testEntityRepository;

  @AfterEach
  @BeforeEach
  void cleanDatabase() {
    getDb().getCollection("testEntity").drop();
  }

  @Test
  void shouldSaveToDatabase() {
    testEntityRepository.save(
        new TestEntity()
            .setId("id_1")
            .setUniqueKey("unique-1")
            .setZonedDateTime(ZonedDateTime.parse("2021-02-21T17:22:53+01:00[Europe/Paris]")));

    assertThat(getDb().getCollection("testEntity").countDocuments()).isOne();
    assertThat(getDb().getCollection("testEntity").find().first())
        .isEqualTo(
            Document.parse(
                """
                {
                  "_class": "org.sdase.commons.spring.boot.mongodb.test.TestEntity",
                  "_id": "id_1",
                  "uniqueKey": "unique-1",
                  "zonedDateTime": {"$date": "2021-02-21T16:22:53Z"}
                }
                """));
  }

  @Test
  void shouldFindInDatabase() {
    getDb()
        .getCollection("testEntity")
        .insertOne(
            Document.parse(
                """
                {
                  "_class": "org.sdase.commons.spring.boot.mongodb.test.TestEntity",
                  "_id": "id_2",
                  "uniqueKey": "unique-2",
                  "zonedDateTime": {"$date": "2021-02-21T17:21:53Z"}
                }
                """));
    Optional<TestEntity> actual = testEntityRepository.findById("id_2");
    assertThat(actual)
        .isPresent()
        .get()
        .extracting(TestEntity::getId, TestEntity::getUniqueKey, TestEntity::getZonedDateTime)
        .containsExactly("id_2", "unique-2", ZonedDateTime.parse("2021-02-21T17:21:53Z"));
  }

  @Test
  void shouldSaveAndFindInDatabase() {
    testEntityRepository.save(
        new TestEntity()
            .setId("id_3")
            .setUniqueKey("unique-3")
            .setZonedDateTime(ZonedDateTime.parse("2021-02-21T17:22:53+01:00[Europe/Paris]")));
    Optional<TestEntity> actual = testEntityRepository.findById("id_3");
    assertThat(actual)
        .isPresent()
        .get()
        .extracting(TestEntity::getId, TestEntity::getUniqueKey, TestEntity::getZonedDateTime)
        .containsExactly("id_3", "unique-3", ZonedDateTime.parse("2021-02-21T16:22:53Z"));
  }

  MongoDatabase getDb() {
    return mongoClient.getDatabase(mongoConnectionDetails.getConnectionString().getDatabase());
  }
}

Kafka

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.

Integration Test for Kafka Consumer
 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
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.verify;

import java.util.Map;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.kafka.test.KafkaTestApp;
import org.sdase.commons.spring.boot.kafka.test.SomeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.test.context.EmbeddedKafka;

@SpringBootTest(
    classes = KafkaTestApp.class,
    webEnvironment = SpringBootTest.WebEnvironment.MOCK,
    properties = {"management.server.port=0"})
@EmbeddedKafka
class KafkaConsumerIntegrationTest {

  @Autowired KafkaTemplate<String, Object> kafkaTemplate;

  @SpyBean SomeService someService;

  @Value("${app.kafka.consumer.topic}") // same as configured for the consumer
  private String topic;

  @Test
  void shouldDoSomethingWithMessage() {
    String givenMessageKey = "some-key";
    var givenMessageValue = 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 received
    await().untilAsserted(() -> verify(someService).didTheJob(givenMessageValue));
  }
}
Integration Test for Kafka Producer
 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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import static org.assertj.core.api.Assertions.assertThat;

import java.time.Duration;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.kafka.test.KafkaTestApp;
import org.sdase.commons.spring.boot.kafka.test.KafkaTestModel;
import org.sdase.commons.spring.boot.kafka.test.KafkaTestProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.kafka.test.context.EmbeddedKafka;
import org.springframework.kafka.test.utils.KafkaTestUtils;

@SpringBootTest(
    classes = KafkaTestApp.class,
    webEnvironment = SpringBootTest.WebEnvironment.MOCK,
    properties = {"management.server.port=0"})
@EmbeddedKafka
class KafkaProducerIntegrationTest {

  @Autowired EmbeddedKafkaBroker kafkaEmbedded;

  @Autowired KafkaTestProducer kafkaTestProducer;

  @Value("${app.kafka.producer.topic}") // as defined for the producer
  String topic;

  @Test
  void shouldProduceMessage() throws ExecutionException, InterruptedException, TimeoutException {
    KafkaTestModel given = new KafkaTestModel().setCheckInt(1).setCheckString("Hello World!");

    kafkaTestProducer.send(given);

    var actualRecords = consumeRecords(topic);
    assertThat(actualRecords)
        .hasSize(1)
        .first()
        .extracting(ConsumerRecord::value)
        .asString()
        .contains("Hello World!");
  }

  ConsumerRecords<String, String> consumeRecords(String topic) {
    try (var testConsumer = createTestConsumer(topic)) {
      return testConsumer.poll(Duration.ofSeconds(10));
    }
  }

  KafkaConsumer<String, String> createTestConsumer(String topic) {
    KafkaConsumer<String, String> consumer =
        new KafkaConsumer<>(
            KafkaTestUtils.consumerProps(
                kafkaEmbedded.getBrokersAsString(), "test-consumer", "true"),
            new StringDeserializer(),
            new StringDeserializer());
    consumer.subscribe(Set.of(topic));
    return consumer;
  }
}

S3

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.

Test S3 Clients in Spring Boot Application
 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
52
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.INPUT_STREAM;

import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.testing.s3.LocalS3Configuration;
import org.sdase.commons.spring.boot.web.testing.s3.S3Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.S3Object;

@S3Test
@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment.MOCK,
    properties = {"management.server.port=0"})
class S3FileRepositoryIntegrationTest {

  @Autowired S3FileRepository s3FileRepository;

  @Test
  void shouldUploadTextFile(S3Client s3Client, LocalS3Configuration config) {
    var givenKey = "test-file.txt";
    var givenText = "Hello World!";

    s3FileRepository.uploadTextFile(givenKey, givenText);

    var actualContent = s3Client.listObjects(l -> l.bucket(config.bucketName()));
    assertThat(actualContent.contents())
        .hasSize(1)
        .first()
        .extracting(S3Object::key)
        .isEqualTo("test-file.txt")
        .extracting(k -> s3Client.getObject(o -> o.bucket(config.bucketName()).key(k)))
        .asInstanceOf(INPUT_STREAM)
        .hasContent("Hello World!");
  }

  @Test
  void shouldDownloadTextFile(S3Client s3Client, LocalS3Configuration config) {
    var givenKey = "existing-test-file.txt";
    var givenText = "Hello World!";
    s3Client.putObject(
        o -> o.bucket(config.bucketName()).key(givenKey),
        RequestBody.fromBytes(givenText.getBytes(StandardCharsets.UTF_8)));

    String actualText = s3FileRepository.downloadText(givenKey);

    assertThat(actualText).isEqualTo("Hello World!");
  }
}

Test S3 Clients without Spring Context

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.