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.
- 
Group ID:
me.madhead.aws-junit5 - 
Version:
7.1.2 - 
Artifacts:
- 
dynamo-v1Support for injection of
AmazonDynamoDB,AmazonDynamoDBAsync,AmazonDynamoDBStreamsandAmazonDynamoDBStreamsAsyncviaDynamoDBextension. - 
dynamo-v2Support for injection of
DynamoDbClient,DynamoDbAsyncClient,DynamoDbStreamsClientandDynamoDbStreamsAsyncClientviaDynamoDBextension. - 
s3-v1 - 
s3-v2Support for injection of
S3ClientandS3AsyncClientviaS3extension. - 
kinesis-v1Support for injection of
AmazonKinesis,AmazonKinesisAsync,AmazonKinesisFirehoseandAmazonKinesisFirehoseAsyncviaKinesisextension. - 
kinesis-v2Support for injection of
KinesisClient,KinesisAsyncClient,FirehoseClientandFirehoseAsyncClientviaKinesisextension. - 
sns-v1Support for injection of
AmazonSNSandAmazonSNSAsyncviaSNSextension. - 
sns-v2Support for injection of
SnsClientandSnsAsyncClientviaSNSextension. - 
sqs-v1Support for injection of
AmazonSQSandAmazonSQSAsyncviaSQSextension. - 
sqs-v2Support for injection of
SqsClientandSqsAsyncClientviaSQSextension. - 
ses-v1Support for injection of
AmazonSimpleEmailServiceandAmazonSimpleEmailServiceAsyncviaSESextension. - 
ses-v2Support for injection of
SesClientandSesAsyncClientviaSESextension. - 
lambda-v1Support for injection of
AWSLambdaandAWSLambdaAsyncviaLambdaextension. - 
lambda-v2Support for injection of
LambdaClientandLambdaAsyncClientviaLambdaextension. 
 - 
 
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:7.1.2")
    testImplementation("me.madhead.aws-junit5:dynamo-v2:7.1.2")
    testImplementation("me.madhead.aws-junit5:s3-v1:7.1.2")
    testImplementation("me.madhead.aws-junit5:s3-v2:7.1.2")
    testImplementation("me.madhead.aws-junit5:kinesis-v1:7.1.2")
    testImplementation("me.madhead.aws-junit5:kinesis-v2:7.1.2")
    testImplementation("me.madhead.aws-junit5:sns-v1:7.1.2")
    testImplementation("me.madhead.aws-junit5:sns-v2:7.1.2")
    testImplementation("me.madhead.aws-junit5:sqs-v1:7.1.2")
    testImplementation("me.madhead.aws-junit5:sqs-v2:7.1.2")
    testImplementation("me.madhead.aws-junit5:ses-v1:7.1.2")
    testImplementation("me.madhead.aws-junit5:ses-v2:7.1.2")
    testImplementation("me.madhead.aws-junit5:lambda-v1:7.1.2")
    testImplementation("me.madhead.aws-junit5:lambda-v2:7.1.2")
}
2.2. Maven
Specify the dependencies you need as usual:
<dependencies>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>dynamo-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>dynamo-v2</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>s3-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>s3-v2</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>kinesis-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>kinesis-v2</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>sns-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>sns-v2</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>sqs-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>sqs-v2</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>ses-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>ses-v2</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>lambda-v1</artifactId>
        <version>7.1.2</version>
    </dependency>
    <dependency>
        <groupId>me.madhead.aws-junit5</groupId>
        <artifactId>lambda-v2</artifactId>
        <version>7.1.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 implement 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() {
        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.
- 
Allows you to configure
ClientConfigurationby implementingClientConfigurationFactory. - 
Allows you to configure:
- 
ClientOverrideConfigurationby implementingClientOverrideConfigurationFactory. - 
SdkHttpClientby implementingSdkHttpClientFactory. - 
SdkAsyncHttpClientby implementingSdkAsyncHttpClientFactory. 
 - 
 
Here is how you can configure asynchronous Netty-based client to use HTTP 1.1 protocol and trust all certificates:
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 GitHub
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.
GitHub uses services keyword in a job description:
services:
  localstack:
    image: localstack/localstack
    ports:
      - 4566:4566
    env:
      SERVICES: dynamodb,dynamodbstreams,s3,kinesis,firehose,sqs,sns,ses,lambda
LocalStack will start services listed in the SERVICES environment variable.
Before running the test, some seed data needs to be initialized. Here is how it may look like for S3:
- run: |
    dynamo/seed/seed.sh
    s3/seed/seed.sh
    kinesis/seed/seed.sh
    sns/seed/seed.sh
    sqs/seed/seed.sh
    ses/seed/seed.sh
    lambda/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:
@ExtendWith(S3.class)
class S3ClientInjectionTest {
    @AWSClient(
        endpoint = Endpoint.class
    )
    private S3Client client;
    @Test
    void test() {
        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.