Add DynamoDB Local to unit tests using Tempest Testing
Zhixuan Lai
Posted on July 2, 2021
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 bysqlite4java
, 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)
Posted on July 2, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.