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 Maven Central Repository.

2.1. Gradle

repositories {
    // Add Maven Central Repository if you don't use it already
    mavenCentral()
}

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

2.2. Maven

Specify the dependencies you need as usual:

pom.xml
<dependencies>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>dynamo-v1</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>dynamo-v2</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>s3-v1</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>s3-v2</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>kinesis-v1</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>kinesis-v2</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>sns-v1</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>sns-v2</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>sqs-v1</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>sqs-v2</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>ses-v1</artifactId>
    <version>6.0.1</version>
</dependency>
<dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>ses-v2</artifactId>
    <version>6.0.1</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,sns,ses
  AWS_CBOR_DISABLE: 'true'
  CBOR_ENABLED: 'false'
…

Before running the test, some seed data needs to be initialized. Here is how it may look like for S3:

gitlab-ci.yml
job:
…
  before_script:
…
    - s3/seed/seed.sh
…
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.