Server Side Chat: Ephemeral environments for your Spring Boot E2E tests

In this Server Side Chat, João Paulo Gomes explored the benefits of isolating tests within ephemeral environments to improve reproducibility and consistency in Spring Boot projects.

Have you ever tried running E2E (end-to-end) tests in your development, test, staging, pre-production (you name it) and had flaky tests and results you could not trust? You spend hours creating a scenario in this environment, and another team has consumed your test data?

Let's explore how to use ephemeral environments to test APIs developed with Spring Boot. But first, what is ephemeral? Ephemeral means lasting for a very short time. Instead of running a test environment constantly and sharing it with different teams, every developer can launch a temporary test environment to run their tests. The environment will shut down after running the tests.

The usual flow from local to prod

The developer uses its computer as a local environment to write code and unit tests.

The code is deployed to a "develop" or test environment where all the other teams do the same. In this environment, the developer can do some exploratory tests.

The code is deployed to a staging environment. Usually, QA people use this environment. They write tests and manage the data necessary to run the tests. Again, this environment is shared between different teams.

Finally, the code is deployed to the production environment.

The flow from the local environment to the production environment.

What are the issues?

The "develop" shared environment is too unstable. This occurs because teams deploy applications that are still in progress and not well-tested. Testing two services together becomes almost impossible when there are dependencies between them.

Different team applications depending on each other.

Finding the correct combination of data is also very difficult because one team can consume the data that another team expects to use. It becomes even more problematic if your test involves more than one service.

These environments also cost money to be maintained. Money that could be invested, for example, in other tools to improve the developer experience.

Improving test reliability

Our goal in this article is to shift your tests left and have as few end-to-end tests as possible, just as the testing pyramid below suggests. I will also show you a tool available since Spring Boot 3.1 that can help you speed up your testing process, having all the pieces under your control. So you can have fast and reliable tests. This tool is the Spring Boot integration with Docker Compose.

This integration can start as many Dockers containers as you need to test your application. It also works during development time. If you run your application's main method, all the containers will be started and stopped automatically for you.

The test pyramid.

Use case #1: one application, two databases

Because the answer to everything in programming is "it depends", let's base this discussion on a use case. Let's start with an easy one:

Use case #1: a service behind an API Gateway that uses Postgres and Redis.

From here on, I assume your application has a good unit test coverage. So, you can start writing an integration test using the @SpringBootTest annotation.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationTests {

    @Autowired
    lateinit var testRestTemplate: TestRestTemplate

    @Test
    fun `should call hello`() {
        val response = testRestTemplate.getForEntity("/hello", String::class.java)

        assertThat(response.statusCode.is2xxSuccessful)
        assertThat(response.body).isNotBlank()
    }
}

You also need to use the Spring Boot Docker Compose integration. Add this to your pom.xml file:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-docker-compose</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>

To make it work, you have to create a Docker Compose file in the root folder of your application, and Spring Boot will start it when your application starts and stop it when your application stops.

services:
  postgres:
    image: 'postgres:latest' 
    environment:
      - 'POSTGRES_DB=mydatabase'
      - 'POSTGRES_PASSWORD=secret'
      - 'POSTGRES_USER=myuser'
    ports:
      - '5432'
  redis:
    image: 'redis:latest'
    ports:
      - '6379'

Always use the same version as you have in the production environment. This way, your tests will run in an environment similar to production. Don't use latest, as you can see in the example.

If you run the test, your application will fail to start. There is only one configuration needed to glue your tests with Docker Compose. Add this to the application.yml file in the test resources folder:

spring:
  docker:
    compose:
      skip:
        in-tests: false

Voila! Your tests are running, and Spring Boot is handling all the plumbing. We did not configure connection strings at any moment because Spring manages everything.

Another benefit is that you can also run it in your pipeline. The pipeline runner needs to support Docker. You can run themvn verify Maven (or gradle verify if you are using Gradle) locally or in your pipeline.

Use case #2: what about the API gateway?

Until now, we have only been testing our application together with the upstream services. What about the downstream ones?

The strategy is the same: add it to your Docker compose file.

You need to add an extra configuration because the API Gateway running inside a Docker container will access the application running on the host machine. In my example, the value added in the Docker Compose file is proxy-host:host-gateway.

  • proxy-host is the name that the host machine will be available inside the API Gateway container.

  • host-gateway is the string that tells Docker to map the host IP address to the name you have defined.

  gateway:
    build:
        context: .
        dockerfile: gateway/Dockerfile
    extra_hosts:
      - proxy-host:host-gateway
    ports:
      - '9090:9090'

I'm building a Docker image from a Maven module project in the example below. This module is a stub from the real gateway. This is nice because I don't have the complexity of pushing my image to a central repository. It's easier to update it if necessary and see the impact right after. This is the Dockerfile:

FROM maven:3.9.6-eclipse-temurin-17 AS build

WORKDIR /app
ADD ../ /app/
RUN mvn clean verify -pl gateway

FROM openjdk:17-jdk-bullseye
COPY --from=build /app/gateway/target/*.jar /app.jar
EXPOSE 9090
ENTRYPOINT java -jar /app.jar

The last change needed is that instead of calling your application directly in your tests, you will call the API Gateway. So, change the port from 8080 to 9090.

Use case #3: what about third-party services?

Use case #3: using a third-party service

In this scenario, you have some options:

  1. Use the same strategy as the API Gateway, creating a stub and building it in a Docker file inside the Docker Compose. You don't need the host-gateway configuration.

  2. Use the wiremock/wiremock Docker image and configure the mappings in the host machine. This is a configuration example:

services:
  third-party:
    image: 'wiremock/wiremock'
    volumes:
      - './third-party/mappings:/home/wiremock/mappings'
    ports:
      - '9091:8080'

Use case #4: what about cloud resources?

Use case #4: Using cloud resources.

If you are using AWS, you can use LocalStack. This is an emulator of the AWS Cloud. You can connect the Java AWS SDK, Terraform, or the Spring AWS integrations with it. You just need to override the endpoint to point it to LocalStack. Example:

@Configuration
@EnableScheduling
class SqsConfiguration {
    @Bean
    fun sqsClient(): SqsClient {
        val credentials = AwsBasicCredentials.create("test", "test")

        return SqsClient.builder()
            .region(Region.US_EAST_1)
            .endpointOverride(URI.create("http://localhost:4566"))
            .credentialsProvider { credentials }
            .build()
    }
}

This is how to enable SQS in LocalStack in the Docker Compose file:

services:
  aws:
    image: 'localstack/localstack:latest'
    environment:
      - SERVICES=sqs
      - AWS_DEFAULT_REGION=eu-central-1
      - EDGE_PORT=4566
    ports:
      - "127.0.0.1:4566:4566"
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Conclusion

As we can see, we have many options to shift testing left and cover as many scenarios as possible. This way, you avoid flaky tests that will not help your team and will probably be disabled or ignored, causing a big waste of time.

We also saw the nice integration between Spring Boot and Docker Compose, making it easy to add, start, and stop the dependencies your project needs to run. It doesn't only work for your tests but also for running your application locally. This is the ephemeral environment mentioned in the title.

This strategy, combined with a nice deployment strategy like canary deployment, feature toggles, or beta testers, can mitigate the issues that happen in production. One thing is sure: issues will happen. You must be prepared with nice monitoring tools and a strategy to roll back quickly. The blue-green deployment is a good alternative to that.

When you find an issue in the production environment, do the rollback and write a test for the problem so you can see it happening in your local machine, and then you can fix it.

The source code of the use cases is available on my GitHub.

Join us in building a strong community

Our Service Side Chats are more than presentations; they're a chance to connect with like-minded professionals, share experiences, and learn from each other. Join us at our next event on November 7.

Through our Service Side Chat community, we aim to foster a vibrant space for professionals to share knowledge, explore innovative ideas, and collaborate on server-side technology projects. We invite everyone to join us on Meetup and become part of this growing community.

Stay tuned for more insightful discussions and be part of our journey.

Thanks for reading.
Now let's get to know each other.

What we do

WAES supports organizations with various solutions at every stage of their digital transformation.

Discover solutions

Work at WAES

Are you ready to start your relocation to the Netherlands? Have a look at our jobs.

Discover jobs

Let's shape our future

Work at WAES

Start a new chapter. Join our team of Modern Day Spartans.

Discover jobs

Work with WAES

Camilo Parra Gonzalez

Camilo Parra Gonzalez

Account Manager