1. General information

aws-junit5 is a collection of JUnit 5 extensions that can be used to inject clients for AWS service mocks provided by tools like localstack or DynamoDB Local in your tests. Both AWS Java SDK v 2.x and v 1.x are supported. Currently these services are supported:

2. Dependency Metadata

Artifacts are deployed to JCenter.

2.1. Gradle

repositories {
    // Add the JCenter if you don't use it already
    jcenter()
}

dependencies {
    // Choose the dependencies you need
    testImplementation("by.dev.madhead.aws-junit5:dynamo-v1:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:dynamo-v2:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:s3-v1:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:s3-v2:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:kinesis-v1:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:kinesis-v2:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:sns-v1:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:sns-v2:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:sqs-v1:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:sqs-v2:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:ses-v1:5.0.2")
    testImplementation("by.dev.madhead.aws-junit5:ses-v2:5.0.2")
}

2.2. Maven

JCenter repository must be added. They recommend to use settings.xml for that:

settings.xml
<settings>
    <profiles>
        <profile>
            <repositories>
                <repository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>central</id>
                    <name>bintray</name>
                    <url>https://jcenter.bintray.com</url>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>central</id>
                    <name>bintray-plugins</name>
                    <url>https://jcenter.bintray.com</url>
                </pluginRepository>
            </pluginRepositories>
            <id>bintray</id>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>bintray</activeProfile>
    </activeProfiles>
</settings>

Then specify the dependencies you need as usual:

pom.xml
<dependencies>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>dynamo-v1</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>dynamo-v2</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>s3-v1</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>s3-v2</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>kinesis-v1</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>kinesis-v2</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>sns-v1</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>sns-v2</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>sqs-v1</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>sqs-v2</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>ses-v1</artifactId>
    <version>5.0.2</version>
</dependency>
<dependency>
    <groupId>by.dev.madhead.aws-junit5</groupId>
    <artifactId>ses-v2</artifactId>
    <version>5.0.2</version>
</dependency>
</dependencies>

3. Usage

3.1. Providing essential parameters

In order to use an AWS service you basically need 4 essential parameters:

  • URL

  • Region

  • Access key

  • Secret key

Not all of them are required, for example DynamoDB Local allows empty keys. But you are not locked to "fake" AWS implementations in your tests, you can use real endpoints as well.

To provide these values you need to imlement AWSEndpoint. Tests could take the values from the environment or system variables, following the twelve-factor principles:

public static class Endpoint implements AWSEndpoint {
    @Override
    public String url() {
        return System.getenv("DYNAMODB_URL");
    }

    @Override
    public String region() {
        return System.getenv("DYNAMODB_REGION");
    }

    @Override
    public String accessKey() {
        return System.getenv("DYNAMODB_ACCESS_KEY");
    }

    @Override
    public String secretKey() {
        return System.getenv("DYNAMODB_SECRET_KEY");
    }
}

3.2. Basic usage

Annotate your test classes, eligible for clients injections, with a corresponding extensions. For the list of supported extensions and clients refer to dependency metadata section.

Finally, put AWSClient annotation on the fields to be injected.

@ExtendWith(DynamoDB.class)
class AmazonDynamoDBInjectionTest {
    @AWSClient(
        endpoint = Endpoint.class
    )
    private AmazonDynamoDB client;

    @Test
    void test() throws Exception {
        Assertions.assertNotNull(client);

        Assertions.assertEquals(
            Collections.singletonList("table"),
            client.listTables().getTableNames().stream().sorted().collect(Collectors.toList())
        );
    }
}

3.3. Advanced configuration

Sometimes, you need extra configuration. For example, when you need to tune HTTP(s) protocol to change timeouts or trust self-signed certificates. @AWSAdvancedConfiguration annotation can be used to provide them. Client configuration differs in AWS Java SDK v 1.x and 2.x, so there are two annotations with this name, one per AWS Java SDK major version.

Here is how you can configure asynchronous Netty-based client to use HTTP 1.1 protocol and trust all certificats:

public class KinesisSdkAsyncHttpClientFactory implements SdkAsyncHttpClientFactory {
    @Override
    public SdkAsyncHttpClient create() {
        return NettyNioAsyncHttpClient
            .builder()
            .protocol(Protocol.HTTP1_1)
            .buildWithDefaults(
                AttributeMap
                    .builder()
                    .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, java.lang.Boolean.TRUE)
                    .build()
            );
    }
}

And then use it:

@AWSClient(
    endpoint = Endpoint.class
)
@AWSAdvancedConfiguration(
    sdkAsyncHttpClientFactory = KinesisSdkAsyncHttpClientFactory.class
)
private KinesisAsyncClient client;

3.4. [Bonus]: CI with GitLab

This projects itself has tests. It uses localstack to mock AWS services. Here is how it works.

First, you need to tell your CI server to start localstack whenever it runs tests. GitLab uses services keyword in a job description:

gitlab-ci.yml
job:
…
  services:
    - name: localstack/localstack
      alias: localstack
…

localstack with start services listed in the SERVICES environment variable, so I define them in the CI config as well:

gitlab-ci.yml
variables:
  SERVICES: dynamodb,dynamodbstreams,s3,kinesis,firehose,sqs
  AWS_CBOR_DISABLE: 'true'
  CBOR_ENABLED: 'false'
…

Before running the test, some seed data needs to be initialized. And before the data can be initialized, you need to be sure that the corresponding service is started. Here is how it looks for S3:

gitlab-ci.yml
job:
…
  before_script:
…
    - timeout 120 bash -c 'until echo > /dev/tcp/localstack/4572; do sleep 1; done'
    - s3/seed/seed.sh
…

First, we wait 120 seconds for S3 to be up and running and then seed it using a script:

s3/seed/seed.sh
#!/usr/bin/env sh

set -x

aws --endpoint-url ${S3_URL} s3api delete-bucket --bucket bucket || true
aws --endpoint-url ${S3_URL} s3api create-bucket --bucket bucket

Everything is ready to be tested now:

S3ClientInjectionTest.java
@ExtendWith(S3.class)
class S3ClientInjectionTest {
    @AWSClient(
        endpoint = Endpoint.class
    )
    private S3Client client;

    @Test
    void test() throws Exception {
        Assertions.assertNotNull(client);

        Assertions.assertEquals(
            Collections.singletonList("bucket"),
            client
                .listBuckets()
                .buckets()
                .stream()
                .map(Bucket::name)
                .sorted()
                .collect(Collectors.toList())
        );
    }
}

This way your tests are free of any initialization logic, you just get the resources you need prepared and injected for you. Simply changing the Endpoint implementation you can attach any AWS compatible service to your test or even use real ones.