Статьи

Модульное тестирование приложений DynamoDB с использованием JUnit5

В предыдущем посте я описал новый AWS SDK для Java 2, который обеспечивает неблокирующую поддержку ввода-вывода для клиентов Java, вызывающих различные сервисы AWS. В этом посте я расскажу о подходе, которому я руководствовался при тестировании модулей вызовов AWS DynamoDB

Есть несколько способов раскрутить локальную версию DynamoDB —

1. AWS предоставляет локальный DynamoDB

2. Localstack предоставляет возможность раскрутить большое количество сервисов AWS локально

3. Докерская версия DynamoDB Local

4. Dynalite , основанная на узле реализация DynamoDB

Теперь, чтобы иметь возможность выполнить модульное тестирование приложения, мне нужно иметь возможность запустить встроенную версию DynamoDB с использованием одного из этих параметров непосредственно перед выполнением теста, а затем закрыть его после завершения теста. Есть три подхода, которые я выбрал:

1. Использование расширения JUnit 5, которое внутренне вызывает AWS DynamoDB Local и раскручивает его после теста.

2. Использование testcontainers для запуска док- версии DynamoDB Local

3. Использование тестконтейнеров для запуска DynaLite

Расширение JUnit5

Расширение JUnit5 обеспечивает удобную точку подключения для запуска
встроенная версия DynamoDB для тестов. Он работает путем добавления версии DynamoDB Local как зависимости maven:

1
2
3
4
5
dependencies {
    ...
 testImplementation("com.amazonaws:DynamoDBLocal:1.11.119")
    ...
}

Сложность с этой зависимостью состоит в том, что есть нативные компоненты (dll, .so и т. Д.), С которыми взаимодействует DynamoDB Local, и чтобы получить их в нужном месте, я зависим от задачи Gradle:

01
02
03
04
05
06
07
08
09
10
11
12
13
task copyNativeDeps(type: Copy) {
 mkdir "build/native-libs"
 from(configurations.testCompileClasspath) {
  include '*.dll'
  include '*.dylib'
  include '*.so'
 }
 into 'build/native-libs'
}
 
test {
 dependsOn copyNativeDeps
}

который помещает собственные библиотеки в папку build / native-libs, а расширение внутренне устанавливает этот путь как системное свойство:

1
System.setProperty("sqlite4java.library.path", libPath.toAbsolutePath().toString())

Вот кодовая база для расширения JUnit5 со всеми этими уже подключенными — https://github.com/bijukunjummen/boot-with dynamicodb / blob / master / src / test / kotlin / sample / dyn / rules / LocalDynamoExtension.kt

Тест с использованием этого расширения выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class HotelRepoTest {
    companion object {
        @RegisterExtension
        @JvmField
        val localDynamoExtension = LocalDynamoExtension()
 
        @BeforeAll
        @JvmStatic
        fun beforeAll() {
            val dbMigrator = DbMigrator(localDynamoExtension.syncClient!!)
            dbMigrator.migrate()
        }
 
    }
    @Test
    fun saveHotel() {
        val hotelRepo = DynamoHotelRepo(localDynamoExtension.asyncClient!!)
        val hotel = Hotel(id = "1", name = "test hotel", address = "test address", state = "OR", zip = "zip")
        val resp = hotelRepo.saveHotel(hotel)
 
        StepVerifier.create(resp)
                .expectNext(hotel)
                .expectComplete()
                .verify()
    }
}

Код может взаимодействовать с полнофункциональной DynamoDB.

TestContainers с локальным докером DynamoDB

Подход расширений JUnit5 работает хорошо, но требует дополнительной зависимости с собственными двоичными файлами. Более чистый подход может заключаться в использовании превосходных Testcontainers для раскрутки док- версии DynamoDB Local следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class HotelRepoLocalDynamoTestContainerTest {
    @Test
    fun saveHotel() {
        val hotelRepo = DynamoHotelRepo(getAsyncClient(dynamoDB))
        val hotel = Hotel(id = "1", name = "test hotel", address = "test address", state = "OR", zip = "zip")
        val resp = hotelRepo.saveHotel(hotel)
 
        StepVerifier.create(resp)
                .expectNext(hotel)
                .expectComplete()
                .verify()
    }
 
 
 
    companion object {
        val dynamoDB: KGenericContainer = KGenericContainer("amazon/dynamodb-local:1.11.119")
                .withExposedPorts(8000)
 
        @BeforeAll
        @JvmStatic
        fun beforeAll() {
            dynamoDB.start()
        }
 
        @AfterAll
        @JvmStatic
        fun afterAll() {
            dynamoDB.stop()
        }
 
        fun getAsyncClient(dynamoDB: KGenericContainer): DynamoDbAsyncClient {
            val endpointUri = "http://" + dynamoDB.getContainerIpAddress() + ":" +
                    dynamoDB.getMappedPort(8000)
            val builder: DynamoDbAsyncClientBuilder = DynamoDbAsyncClient.builder()
                    .endpointOverride(URI.create(endpointUri))
                    .region(Region.US_EAST_1)
                    .credentialsProvider(StaticCredentialsProvider
                            .create(AwsBasicCredentials
                                    .create("acc", "sec")))
            return builder.build()
        }
 
        ...
    }
}

Этот код запускает DynamoDB со случайного незанятого порта и предоставляет эту информацию, чтобы клиент мог быть создан с использованием этой информации. Есть небольшой обходной путь Kotlin, который я должен был сделать, основываясь на проблеме, о которой здесь сообщалось .

ТестКонтейнеры с Dynalite

Dynalite является реализацией DynamoDB на основе JavaScript и может быть снова запущен для тестов с использованием подхода TestContainer. Однако на этот раз уже есть модуль TestContainer для Dynalite . Я обнаружил, что он не поддерживает JUnit5, и отправил запрос Pull для обеспечения этой поддержки, в нем можно использовать необработанный образ докера, и вот как выглядит тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class HotelRepoDynaliteTestContainerTest {
    @Test
    fun saveHotel() {
        val hotelRepo = DynamoHotelRepo(getAsyncClient(dynamoDB))
        val hotel = Hotel(id = "1", name = "test hotel", address = "test address", state = "OR", zip = "zip")
        val resp = hotelRepo.saveHotel(hotel)
 
        StepVerifier.create(resp)
                .expectNext(hotel)
                .expectComplete()
                .verify()
    }
 
    companion object {
        val dynamoDB: KGenericContainer = KGenericContainer("quay.io/testcontainers/dynalite:v1.2.1-1")
                .withExposedPorts(4567)
 
        @BeforeAll
        @JvmStatic
        fun beforeAll() {
            dynamoDB.start()
            val dbMigrator = DbMigrator(getSyncClient(dynamoDB))
            dbMigrator.migrate()
        }
 
        @AfterAll
        @JvmStatic
        fun afterAll() {
            dynamoDB.stop()
        }
 
        fun getAsyncClient(dynamoDB: KGenericContainer): DynamoDbAsyncClient {
            val endpointUri = "http://" + dynamoDB.getContainerIpAddress() + ":" +
                    dynamoDB.getMappedPort(4567)
            val builder: DynamoDbAsyncClientBuilder = DynamoDbAsyncClient.builder()
                    .endpointOverride(URI.create(endpointUri))
                    .region(Region.US_EAST_1)
                    .credentialsProvider(StaticCredentialsProvider
                            .create(AwsBasicCredentials
                                    .create("acc", "sec")))
            return builder.build()
        }
        ...
    }
}

Вывод

Все подходы полезны для возможности тестирования интеграции с DynamoDB. Мое личное предпочтение — использовать подход TestContainers, если агент докера доступен еще с подходом расширения JUnit5. Образцы с полностью работающими тестами, использующими все три подхода, доступны в моем репозитории github — https://github.com/bijukunjummen/boot-with-dynamodb

Смотрите оригинальную статью здесь: модульное тестирование приложений DynamoDB с использованием JUnit5

Мнения, высказанные участниками Java Code Geeks, являются их собственными.