Add DynamoDB Local to unit tests using Tempest Testing

zhxnlai

Zhixuan Lai

Posted on July 2, 2021

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 }}"
}
Enter fullscreen mode Exit fullscreen mode

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 }}"
}
Enter fullscreen mode Exit fullscreen mode

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...
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

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

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 }}"
}
Enter fullscreen mode Exit fullscreen mode

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 }}"
}
Enter fullscreen mode Exit fullscreen mode

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...
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

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

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()
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
💖 💪 🙅 🚩
zhxnlai
Zhixuan Lai

Posted on July 2, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related