Add DynamoDB Local to unit tests using Tempest 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:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest-testing-junit5:{{ versions.tempest }}"
    }
    // Or
    dependencies {
      testImplementation "app.cash.tempest:tempest-testing-docker:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest-testing-junit5:{{ versions.tempest }}"
    }
    For AWS SDK 2.x:
    dependencies {
      testImplementation "app.cash.tempest:tempest2-testing-jvm:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest2-testing-junit5:{{ versions.tempest }}"
    }
    // Or
    dependencies {
      testImplementation "app.cash.tempest:tempest2-testing-docker:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest2-testing-junit5:{{ versions.tempest }}"
    }
    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...
      }
    }
    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:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest-testing-junit4:{{ versions.tempest }}"
    }
    // Or
    dependencies {
      testImplementation "app.cash.tempest:tempest-testing-docker:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest-testing-junit4:{{ versions.tempest }}"
    }
    For AWS SDK 2.x:
    dependencies {
      testImplementation "app.cash.tempest:tempest2-testing-jvm:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest2-testing-junit4:{{ versions.tempest }}"
    }
    // Or
    dependencies {
      testImplementation "app.cash.tempest:tempest2-testing-docker:{{ versions.tempest }}"
      testImplementation "app.cash.tempest:tempest2-testing-junit4:{{ versions.tempest }}"
    }
    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...
      }
    }
    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)
  • 19

    This website collects cookies to deliver better user experience

    Add DynamoDB Local to unit tests using Tempest Testing