Showing posts with label integration testing. Show all posts
Showing posts with label integration testing. Show all posts

Monday, April 13, 2026

Alternatives for LocalStack in TestContainers with Spring Boot

Introduction

Since around 20 March 2026 the company behind LocalStack decided to not support the community edition of LocalStack anymore, only paid versions are now available. 


A lot of forks were made from that now-frozen community repository, like this one.  Will any of these become the new community supported version?

Many teams use LocalStack within TestContainers in a Spring Boot application for example. What are the alternatives? This post is about exploring those.

Note that you can also pin the version of LocalStack to the last community edition release 4.14.0, that will keep on working after 6 april 2026. But there won’t be any updates on it of course…

Two forms are emerging: either replace the whole AWS stack as much as possible with one framework, or take an separate implementation per AWS service, so: implementation A to support one specific AWS service and implementation B for another AWS service, etc.

Below I’ve split my analysis in those two forms. 



Full AWS stack replacements


Several options exist for full AWS stack replacements.

Floci

 

Floci is a free, open-source local AWS emulator which can be found at https://github.com/floci-io/floci
It is a very recently created project , but got quite some traction already.  
A week ago Floci changed into a separate organisation, see this LinkedIn post
It is not a ful drop-in replacement in TestContainers, but it comes close. Below shows what needs changing in a Kotlin Spring Boot application if you are currently using LocalStack.

Replacing LocalStack 

 

You probably have something like this currently configured with LocalStack:

@TestConfiguration

class TestConfig {

  @Bean

  fun localStackContainer(): LocalStackContainer {

    return LocalStackContainer(DockerImageName.parse("localstack/localstack:4:14:0"))

      .withServices(LocalStackContainer.Service.DYNAMODB)

  }


Basically changing it by replacing the docker image name and the environment variables should get it working already you’d think, so like this:

@TestConfiguration

class TestConfig {

@Bean

fun localStackContainer(): LocalStackContainer {

  return LocalStackContainer(DockerImageName.parse(hectorvent/floci:1.5.2).asCompatibleSubstituteFor("localstack/localstack"))

    .withEnv("AWS_ENDPOINT_URL", "http://localhost:4566")

    .withEnv("AWS_DEFAULT_REGION", "us-east-1")

    .withEnv("AWS_ACCESS_KEY_ID", "test")

    .withEnv("AWS_SECRET_ACCESS_KEY", "test")

}


But no it is not that easy. You’ll need to insert a different startup-script.
For example like this:

class FlociContainer(dockerImageName: DockerImageName) :   LocalStackContainer(dockerImageName) {


override fun containerIsStarting(containerInfo: InspectContainerResponse) {

  var shell: String

  var executable: String

  if (dockerImageName.contains("jvm")) {

    shell = "/bin/sh"

    executable = "java -jar quarkus-app/quarkus-run.jar"

  } else {

    shell = "/bin/bash"

    executable = "./application"

  }

  var command = shell + "\n" + executable + "\n"

  try {

    copyFileToContainer(Transferable.of(command, 511), STARTER_SCRIPT)

  } catch (e: Throwable) {

    logger.error("Failed to copy startup script to container: ${e.message}", e)

    throw e

  }

}

…

}

 

Note the logic to determine the correct command for the startup-script to use in the Docker image. Floci has a native version which has a different startup-script than the JVM version.
You’ll also need to parse for a different startup string than LocalStack has.
TestContainers has a module for LocalStack that looks for this string: ".*Ready\\.\n"
But Floci does not log that string, so one to search for is: ".*started in.*"
So you need to override that for the Floci version. 


Then the test configuration becomes this:

@TestConfiguration

class TestConfig {

@Bean

fun localStackContainer(): LocalStackContainer {

  val flociImage: DockerImageName =

  DockerImageName.parse(hectorvent/floci:1.5.2).asCompatibleSubstituteFor("localstack/localstack")

 

  return FlociContainer(flociImage)

          // Floci settings

    .withEnv("AWS_ENDPOINT_URL", "http://localhost:4566")

    .withEnv("AWS_DEFAULT_REGION", "us-east-1")

    .withEnv("AWS_ACCESS_KEY_ID", "test")

    .withEnv("AWS_SECRET_ACCESS_KEY", "test")

          // Disable services you don’t need

    .withEnv("FLOCI_SERVICES_SSM_ENABLED", "false")

    .withEnv("FLOCI_SERVICES_ELASTICSEARCH_ENABLED", "false")

           // Floci container has different log-text to look for

    .waitingFor(LogMessageWaitStrategy().withRegEx(".*started in.*").withTimes(1))

}

 

That should do it!


Building Docker JVM image of Floci and start it locally 

 

For building a Docker image for the JVM version, you can run these commands in the root directory of the Floci project: 

  1. Create the artifact (application) for the Docker image: ./mvnw clean package
  2. Build the docker image: docker build . -t floci-jvm-package:1.5.1-fix-001 -f Dockerfile.jvm-package 


To start the JVM image of Floci locally you can run this command in the root directory:

 

docker-compose -f docker-compose-fix-001.xml up

 

With this in that docker compose file:

 

services:

    floci:

        image:    floci-jvm-package:1.5.1-fix-001

        ports:

            - "4566:4566"

        volumes:

            - ./data:/app/data

            - ./init/start.d:/etc/floci/init/start.d:ro

            - ./init/stop.d:/etc/floci/init/stop.d:ro


The output will look something like this then:

✔ Container common-adapters-floci-1    Recreated                                                                                                                                                                                                                                                                                                                          0.1s

Attaching to floci-1

floci-1    |      ______ _            ____      _____ _____

floci-1    |    |    ____| |        / __ \ / ____|_      _|

floci-1    |    | |__    | |      | |    | | |            | |   

floci-1    |    |    __| | |      | |    | | |            | |   

floci-1    |    | |        | |___| |__| | |____ _| |_

floci-1    |    |_|        |______\____/ \_____|_____|

floci-1    |

floci-1    |

floci-1    |                      Powered by Quarkus 3.32.3

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) === AWS Local Emulator Starting ===

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) Storage mode: memory

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) Persistent path: ./data

floci-1    | 2026-04-11 15:51:14,012 INFO    [io.github.hectorvent.floci.core.common.ServiceRegistry] (main) Enabled services: [ssm, sqs, s3, dynamodb, sns, lambda, apigateway, iam, elasticache, rds, events, scheduler, logs, monitoring, secretsmanager, apigatewayv2, kinesis, kms, cognito-idp, states, cloudformation, acm, email, es]

floci-1    | 2026-04-11 15:51:14,013 INFO    [io.github.hectorvent.floci.lifecycle.EmulatorLifecycle] (main) === AWS Local Emulator Ready ===

floci-1    | 2026-04-11 15:51:14,021 INFO    [io.quarkus] (main) floci 1.4.0 native (powered by Quarkus 3.32.3) started in 0.085s. Listening on: http://0.0.0.0:4566

floci-1    | 2026-04-11 15:51:14,021 INFO    [io.quarkus] (main) Profile prod activated.

floci-1    | 2026-04-11 15:51:14,021 INFO    [io.quarkus] (main) Installed features: [cdi, config-yaml, rest, rest-jackson, smallrye-context-propagation, vertx]


Notice also the ‘started in 0.085s’, which contains the string for which the above example FlociContainer is looking for to see if the container started successfully.


Automatically create your application queues in Floci


A fancy enhancement for when you are using Spring Boot: you can write a BeanFactoryPostProcessor which parses for @SqsListener annotations to automatically create the queues in the Floci container.

Run Integration Tests against locally running Floci


To run your tests against a locally running Floci instance directly, for example to see its logging, started up with Docker like mentioned above, you override the endpoint when creating the DynamoDBClient:

    return DynamoDbClient.builder()

          .endpointOverride(URI.create("http://localhost:4566"))

    .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(localStackContainer.accessKey, localStackContainer.secretKey)))

    .region(Region.of(localStackContainer.region))

    .build()

}


Testcontainers module for Floci

 

There’s now also a testcontainers module available for floci so you should not need all above manual changes: https://testcontainers.com/modules/floci/

I did not test this module yet.

 



Other full replacement options

  • Rust focused it seems: Rustack
  • Only 1 maintainer and runs standalone, some AWS, some GCP, some Azure services supported: CloudTwin
  • Python focused it seems: Moto: This post talks about Moto mocking and using it in unittests. Is it a mocking or emulating framework?
  • Any of the LocalStack forks.


AWS stack per-service replacement


Multiple options exist, this list might not be listing all options. 

  1. DynamoDB: 
    1. use the AWS DynamoDB Local replacement. Docker image is here. Can be used on its own, and there’s a TestContainers module for it too here.
    2. Dynalite: last update in 2020, seems to be dead. The TestContainers module seems to be gone now too
  2. S3: Minio Not supported anymore, see here.
  3. SQS: https://lib.rs/crates/tc_elasticmq. Docker image here.






Wednesday, April 6, 2022

Configuring MySQL test-containers in your Spring Boot Java Integration Tests

Introduction

In your Integration Tests (IT) you often try to use the H2 in-memory database, to improve the speed of your integration tests. But on the other hand you want to mimic the production database as much as possible in your integration-tests.

Setting H2 in MySQL database compatibility mode tries to emulate MySQL as much as possible, but only a small subset of the differences are implemented. What for example not works correctly in H2 for JSON fields is that it escapes strings with "". For that reason you usually want to switch to for example starting a Docker database testcontainer in your IT tests, which uses a real MySQL database. With the Java-specific version in https://github.com/testcontainers/testcontainers-java.


Configuration

There are several good-to-know tips when configuring the testcontainers in your ITs.

  1. The simplest configuration is using a datasource URL in the Spring Boot properties file. This has the disadvantage that whatever database name you specify, the testcontainers library still creates a DB named 'test'. So below it will be named 'integration_test_db' you'd think, but it is still named 'test' when the IT runs:

    spring.datasource.url=jdbc:tc:mysql:5.7.32:///integration_test_db?sessionVariables=sql_mode='STRICT_TRANS_TABLES'&TC_MY_CNF=mysql&TC_INITSCRIPT=mysql/init_mysql_integration_tests.sql

    To be able to do everything on the started database, including giving it the name you want, use this URL (or see below the Java version). Notice the user 'root' in the URL:
    spring.datasource.url=jdbc:tc:mysql:5.7.32:///integration_test_db?user=root&password=&sessionVariables=sql_mode='STRICT_TRANS_TABLES'&TC_MY_CNF=mysql&TC_INITSCRIPT=mysql/init_mysql_integration_tests.sql

    Via: https://github.com/testcontainers/testcontainers-java/issues/932

  2. To initialize your database, specify the script via this extra datasource URL variable:

    TC_INITSCRIPT=mysql/init_mysql_integration_tests.sql

    Note the default directory it looks in is ..../resources for the scripts. So the full path is ..../resources/mysql/

  3. An example to prevent GROUP BY error from strict mode, add this in your TC_INITSCRIPT:

    SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES';
    SET SESSION sql_mode = 'STRICT_TRANS_TABLES';


  4. To configure it in the IT Java class itself:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = { SomeClassA.class, SomeClassB.class, ApplicationConfiguration.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @TestPropertySource(locations = {
            "classpath:/application-test-mysql.properties" })
    @ContextConfiguration(initializers = {ThisITClass.Initializer.class})

    static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
            public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
                TestPropertyValues.of(
                        "spring.datasource.url=" + mySQLContainer.getJdbcUrl(),
                        "spring.datasource.username=" + mySQLContainer.getUsername(),
                        "spring.datasource.password=" + mySQLContainer.getPassword()
                ).applyTo(configurableApplicationContext.getEnvironment());
            }
        }

    @ClassRule
    public static MySQLContainer mySQLContainer = new MySQLContainer<>("mysql:5.7.31")
                .withUsername("root") // So now you can do a GRANT too for example
                .withPassword("") // Only possible for user 'root'
                .withEnv("MYSQL_ROOT_HOST", "%")
                .withDatabaseName("integration_test_db") // So this name will now be used, not 'test'
                .withInitScript("mysql/init_mysql_integration_tests.sql")

                ;

  5. The MySQL docker image used in the tests is retrieved from DockerHub https://hub.docker.com/_/mysql

Saturday, February 2, 2013

Lessons learned Spring 3.0 project

Another quick post with some bullet points of lessons learned during my last Spring 3.0 project.
Tools used were:

  • Spring 3.0 (3.1.2 when moving to Hibernate 4)
  • Spring MVC
  • Spring WebFlow 2
  • Spring Batch
  • Spring Agent Framework
  • Hibernate 3.2.6
  • Apache CXF
  • JBehave (Behavior Driven Development)
  • Tiny bit of GXT, which is a Google Web Toolkit extension based on GWT and ExtJS technology. Formally known as Ext GWT, GXT is a Java library for building rich internet applications with the Google Web Toolkit (GWT)
  • JBoss jBPM 5.2.0
  • JBoss Drools 5.3.1
  • Jenkins for CI
  • Crucible for code reviews
  • Jira for issue tracking en sprint user stories and tasks and progress tracking
  • SpringSource Tool Suite 2.9.1 as Eclipse based IDE
  • JUnit 4.8
  • EasyMock 2.5.2, EasyMockSupport
  • Java 6
  • Cargo-itest (introduction here) for integration testing
  • Tomcat 6
  • Oracle SQL Developer (free version)
Besides learning new tools like JBehave an Cargo-itest, below are some things picked up using above tools.

Spring MVC
Had some issues getting REST-like services available, mainly from inexperience, but good to know what's working and what not:

    @GET
    @Path("some-path/?role={roleId}")
    Response getSomeOtherUrlParams(@PathParam(value = "roleId") final String roleId);
    // Unable to call it, because PathParam is not existing (it's not really part of the path)

    @GET
    @Path("some-path2/role/{roleId}")
    Response getSomeOtherUrlParamsNotAsRequest(@PathParam(value = "roleId") final String roleId);
    // Works

    @GET
    @Path("some-path3/")
    Response getSomeOtherUrlParamsAsQueryParam(@QueryParam("role") final String roleId);
    // Works, will have clients of this service (like SoapUI) already generate a ?role= parameter

    @GET
    @Path("some-path4/")
    Response getSomeOtherUrlParamsAsRequestParam(@RequestParam("role") final String roleId);
    // Won't generate a ?role= parameter for clients, the WADL also looks different for this method/parameter

Don't use @FormParam for submitting XML because if the mediatype is text/xml for example, the mapping on the REST method fails because JAXRS expects HTML (default if not specified at the service definition).
So suppose the service is defined as:

    @POST
    @Path("message/{messageCode}/send")
    Response sendMessage(@PathParam(value = "messageCode") final String messageCode, 
                                         @FormParam("moduleName") String moduleName,
                                         @FormParam("userId") String userId, 
                                         final MessageContentsRequest messageContents);

When invoking the above method by POSTing to 

http://localhost:8080/webservice/v1/message/SECRET_MESSAGE_CODE/send 

with as POST body parameters moduleName and userId + the MessageContentsRequest XML, that gives the following error in the log:

2013-01-05 11:24:38,491 WARN  [http-8890-1] JAXRSUtils#processFormParam - An application/x-www-form-urlencoded form request is expected but the request media type is text/xml;charset=UTF-8. Consider removing @FormParam annotations.
2013-01-05 11:24:38,493 WARN  [http-8890-1] WebApplicationExceptionMapper#toResponse - WebApplicationException has been caught : no cause is available

The same error occurs when invoking: ..../send/?moduleName=abc&userId=ttl  (so adding them as POST URL name/value parameters).

So make sure you use in that case:

    @POST
    @Path("message/{messageCode}/send")
    Response sendMessage(@PathParam(value = "messageCode") final String message ode, 
                                         @QueryParam("moduleName") String moduleName,
                                         @QueryParam("userId") String userId, 
                                         final MessageContentsRequest messageContents);

The moduleName and userId can then be passed in via the POST URL name/value parameters.


EasyMock
Use replayAll() and verifyAll() instead of replay(mock) and verify(mock) for each mock seperately. Requires extending EasyMockSupport.
 


SQL Developer
The free SQL Developer can't export clob column values. Nor can it import clob values larger than 4K.




 

Sunday, January 31, 2010

Best of this Week Summary 25 January - 31 January 2010

Sunday, April 19, 2009

Best of this Week Summary 13 April - 19 April 2009