Skip to content

Testing

Tempest Testing

Tempest provides a library for testing DynamoDB clients using DynamoDBLocal . It comes with two implementations:

  • JVM: This is the preferred option, running a DynamoDBProxyServer backed by sqlite4java, which is available on most platforms.
  • Docker: This runs dynamodb-local in a Docker container.

Feature matrix:

Feature tempest-testing-jvm tempest-testing-docker
Start up time ~1s ~10s
Memory usage Less More
Dependency sqlite4java native library Docker

JUnit 5 Integration

To use tempest-testing, first add this library as a test dependency:

For AWS SDK 1.x:

dependencies {
  testImplementation "app.cash.tempest:tempest-testing-jvm:1.10.0"
  testImplementation "app.cash.tempest:tempest-testing-junit5:1.10.0"
}
// Or
dependencies {
  testImplementation "app.cash.tempest:tempest-testing-docker:1.10.0"
  testImplementation "app.cash.tempest:tempest-testing-junit5:1.10.0"
}

For AWS SDK 2.x:

dependencies {
  testImplementation "app.cash.tempest:tempest2-testing-jvm:1.10.0"
  testImplementation "app.cash.tempest:tempest2-testing-junit5:1.10.0"
}
// Or
dependencies {
  testImplementation "app.cash.tempest:tempest2-testing-docker:1.10.0"
  testImplementation "app.cash.tempest:tempest2-testing-junit5:1.10.0"
}

Then in tests annotated with @org.junit.jupiter.api.Test, you may add TestDynamoDb as a test extension. This extension spins up a DynamoDB server. It shares the server across tests and keeps it running until the process exits. It also manages test tables for you, recreating them before each test.

class MyTest {
  @RegisterExtension
  @JvmField
  val db = TestDynamoDb.Builder(JvmDynamoDbServer.Factory)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem.TABLE_NAME, MusicItem::class.java))
      .build()

  private val musicTable by lazy { db.logicalDb<MusicDb>().music }

  @Test
  fun test() {
    val albumInfo = AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    )
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo.save(albumInfo)
  }

  @Test
  fun anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    val result = db.dynamoDb.describeTable(
            DescribeTableRequest.builder().tableName(MusicItem.TABLE_NAME).build()
    )
    // Do something with the result...
  }
}
class MyTest {
  @RegisterExtension
  TestDynamoDb db = new TestDynamoDb.Builder(JvmDynamoDbServer.Factory.INSTANCE)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem.TABLE_NAME, MusicItem.class))
      .build();

  MusicTable musicTable;

  @BeforeEach
  public void setup() {
    musicTable = db.logicalDb(MusicDb.class).music();
  }

  @Test
  public void test() {
    AlbumInfo albumInfo = new AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    );
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo().save(albumInfo);
  }

  @Test
  public void anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    DescribeTableResponse result = db.getDynamoDb().describeTable(
            DescribeTableRequest.builder().tableName(MusicItem.TABLE_NAME).build()
    );
    // Do something with the result...
  }
}
class MyTest {
  @RegisterExtension
  @JvmField
  val db = TestDynamoDb.Builder(JvmDynamoDbServer.Factory)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem::class.java))
      .build()

  private val musicTable by lazy { db.logicalDb<MusicDb>().music }

  @Test
  fun test() {
    val albumInfo = AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    )
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo.save(albumInfo)
  }

  @Test
  fun anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    val result = db.dynamoDb.describeTable("music_items")
    // Do something with the result...
  }
}
class MyTest {
  @RegisterExtension
  TestDynamoDb db = new TestDynamoDb.Builder(JvmDynamoDbServer.Factory.INSTANCE)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem.class))
      .build();

  MusicTable musicTable;

  @BeforeEach
  public void setup() {
    musicTable = db.logicalDb(MusicDb.class).music();
  }

  @Test
  public void test() {
    AlbumInfo albumInfo = new AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    );
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo().save(albumInfo);
  }

  @Test
  public void anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    DescribeTableResult result = db.getDynamoDb().describeTable("music_items");
    // Do something with the result...
  }
}

To customize test tables, mutate the CreateTableRequest in a lambda.

fun testDb() = TestDynamoDb.Builder(JvmDynamoDbServer.Factory)
  .addTable(
    TestTable.create<MusicItem> { createTableRequest ->
      for (gsi in createTableRequest.globalSecondaryIndexes) {
        gsi.withProjection(Projection().withProjectionType(ProjectionType.ALL))
      }
      createTableRequest
    }
  )
  .build()

To use the Docker implementation, specify it in the builder.

fun testDb() = TestDynamoDb.Builder(DockerDynamoDbServer.Factory)
  .addTable(TestTable.create<MusicItem>())
  .build()

JUnit 4 Integration

To use tempest-testing, first add this library as a test dependency:

For AWS SDK 1.x:

dependencies {
  testImplementation "app.cash.tempest:tempest-testing-jvm:1.10.0"
  testImplementation "app.cash.tempest:tempest-testing-junit4:1.10.0"
}
// Or
dependencies {
  testImplementation "app.cash.tempest:tempest-testing-docker:1.10.0"
  testImplementation "app.cash.tempest:tempest-testing-junit4:1.10.0"
}

For AWS SDK 2.x:

dependencies {
  testImplementation "app.cash.tempest:tempest2-testing-jvm:1.10.0"
  testImplementation "app.cash.tempest:tempest2-testing-junit4:1.10.0"
}
// Or
dependencies {
  testImplementation "app.cash.tempest:tempest2-testing-docker:1.10.0"
  testImplementation "app.cash.tempest:tempest2-testing-junit4:1.10.0"
}

Then in tests annotated with @org.junit.Test, you may add TestDynamoDb as a test rule. This rule spins up a DynamoDB server. It shares the server across tests and keeps it running until the process exits. It also manages test tables for you, recreating them before each test.

class MyTest {
  @get:Rule
  val db = TestDynamoDb.Builder(JvmDynamoDbServer.Factory)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem.TABLE_NAME, MusicItem::class.java))
      .build()

  private val musicTable by lazy { db.logicalDb<MusicDb>().music }

  @Test
  fun test() {
    val albumInfo = AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    )
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo.save(albumInfo)
  }

  @Test
  fun anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    val result = db.dynamoDb.describeTable(
            DescribeTableRequest.builder().tableName(MusicItem.TABLE_NAME).build()
    )
    // Do something with the result...
  }
}
class MyTest {
  @Rule
  public TestDynamoDb db = new TestDynamoDb.Builder(JvmDynamoDbServer.Factory.INSTANCE)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem.TABLE_NAME, MusicItem.class))
      .build();

  MusicTable musicTable;

  @Before
  public void setup() {
    musicTable = db.logicalDb(MusicDb.class).music();
  }

  @Test
  public void test() {
    AlbumInfo albumInfo = new AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    );
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo().save(albumInfo);
  }

  @Test
  public void anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    DescribeTableResponse result = db.getDynamoDb().describeTable(
            DescribeTableRequest.builder().tableName(MusicItem.TABLE_NAME).build()
    );
    // Do something with the result...
  }
}
class MyTest {
  @get:Rule
  val db = TestDynamoDb.Builder(JvmDynamoDbServer.Factory)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem::class.java))
      .build()

  private val musicTable by lazy { db.logicalDb<MusicDb>().music }

  @Test
  fun test() {
    val albumInfo = AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    )
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo.save(albumInfo)
  }

  @Test
  fun anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    val result = db.dynamoDb.describeTable("music_items")
    // Do something with the result...
  }
}
class MyTest {
  @Rule
  public TestDynamoDb db = new TestDynamoDb.Builder(JvmDynamoDbServer.Factory.INSTANCE)
      // `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
      .addTable(TestTable.create(MusicItem.class))
      .build();

  MusicTable musicTable;

  @Before
  public void setup() {
    musicTable = db.logicalDb(MusicDb.class).music();
  }

  @Test
  public void test() {
    AlbumInfo albumInfo = new AlbumInfo(
        "ALBUM_1",
        "after hours - EP",
        "53 Thieves",
        LocalDate.of(2020, 2, 21),
        "Contemporary R&B"
    );
    // Talk to DynamoDB using Tempest's API.
    musicTable.albumInfo().save(albumInfo);
  }

  @Test
  public void anotherTest() {
    // Talk to DynamoDB using the AWS SDK.
    DescribeTableResult result = db.getDynamoDb().describeTable("music_items");
    // Do something with the result...
  }
}

To customize test tables, mutate the CreateTableRequest in a lambda.

fun testDb() = TestDynamoDb.Builder(JvmDynamoDbServer.Factory)
  .addTable(
    TestTable.create<MusicItem> { createTableRequest ->
      for (gsi in createTableRequest.globalSecondaryIndexes) {
        gsi.withProjection(Projection().withProjectionType(ProjectionType.ALL))
      }
      createTableRequest
    }
  )
  .build()

To use the Docker implementation, specify it in the builder.

fun testDb() = TestDynamoDb.Builder(DockerDynamoDbServer.Factory)
  .addTable(TestTable.create<MusicItem>())
  .build()

Other Testing Frameworks

Tempest testing is compatible with other testing frameworks. You’ll need to write your own integration code. Feel free to reference the implementations above. Here is a simpler example:

import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
// ...

class JUnit5TestDynamoDb(
  private val testTables: List<TestTable>,
) : BeforeEachCallback, AfterEachCallback {

  private val service = TestDynamoDbService.create(JvmDynamoDbServer.Factory, testTables, 8000)

  override fun beforeEach(context: ExtensionContext) {
    service.startAsync()
    service.awaitRunning()
  }

  override fun afterEach(context: ExtensionContext?) {
    service.stopAsync()
    service.awaitTerminated()
  }
}

Check out the code samples on Github:

  • Music Library - SDK 1.x (.kt, .java)
  • Music Library - SDK 2.x (.kt, .java)
  • Testing - SDK 1.x - JUnit4 - JVM (.kt, .java)
  • Testing - SDK 1.x - JUnit4 - Docker (.kt, .java)
  • Testing - SDK 1.x - JUnit5 - JVM (.kt, .java)
  • Testing - SDK 1.x - JUnit5 - Docker (.kt, .java)
  • Testing - SDK 2.x - JUnit4 - JVM (.kt, .java)
  • Testing - SDK 2.x - JUnit4 - Docker (.kt, .java)
  • Testing - SDK 2.x - JUnit5 - JVM (.kt, .java)
  • Testing - SDK 2.x - JUnit5 - Docker (.kt, .java)