更容易的 DynamoDB 本地测试

我正在使用 DynamoDB 本地进行单元测试。它不错,但是有一些缺点。具体来说:

  • 您必须在测试运行之前以某种方式启动服务器
  • 服务器在每次测试之前没有启动和停止,因此测试变得相互依赖,除非在每次测试之后添加代码来删除所有表等
  • 所有开发人员都需要安装它

我想要做的是将 DynamoDB 本地 jar 和它所依赖的其他 jar 放在我的 test/resources目录中(我用 Java 编写)。然后,在每次测试之前,我会启动它,用 -inMemory运行,在测试之后,我会停止它。这样,任何人下载 git 回购都可以获得运行测试所需的所有内容的副本,而且每个测试都是独立于其他测试的。

我已经找到了一个办法,让这个工作,但它的丑陋,所以我正在寻找其他选择。我的解决方案是。然后在 @Before方法中,我将它解压缩到一个临时目录中,并启动一个新的 Java 进程来执行它。这很有效,但它很丑陋,而且有一些缺点:

  • 每个人都需要 $PATH上的 Java 可执行文件
  • 我必须将压缩文件解压缩到本地磁盘。对于测试来说,使用本地磁盘通常是很冒险的,特别是在连续构建等情况下。
  • 我必须生成一个进程,并等待它为每个单元测试启动,然后在每个测试之后终止该进程。除了速度缓慢之外,遗留进程的可能性似乎很小。

看来应该有更简单的办法。毕竟,DynamoDB Local 只是 Java 代码。难道我不能以某种方式要求 JVM 分叉自身并查看内部资源以构建类路径吗?或者,更好的情况是,我不能从其他线程调用 DynamoDB Local 的 main方法,这样就可以在单个进程中完成所有操作了吗?有什么想法吗?

附言: 我知道交流发电机,但它似乎有其他缺点,所以我倾向于坚持亚马逊的支持的解决方案,如果我可以使它的工作。

82713 次浏览

对于工作中的单元测试,我使用 Mockito,然后模仿 AmazonDynamoDBClient。然后使用 when 模拟出返回值。如下:

when(mockAmazonDynamoDBClient.getItem(isA(GetItemRequest.class))).thenAnswer(new Answer<GetItemResult>() {
@Override
public GetItemResult answer(InvocationOnMock invocation) throws Throwable {
GetItemResult result = new GetItemResult();
result.setItem( testResultItem );
return result;
}
});

不知道这是不是你想要的,但我们就是这么做的。

您可以在测试代码中使用 DynamoDBLocal 作为 Maven 测试依赖项,如下面的 公告所示。你可以在 HTTP 上运行:

import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;


final String[] localArgs = { "-inMemory" };
DynamoDBProxyServer server = ServerRunner.createServerFromCommandLineArgs(localArgs);
server.start();
AmazonDynamoDB dynamodb = new AmazonDynamoDBClient();
dynamodb.setEndpoint("http://localhost:8000");
dynamodb.listTables();
server.stop();

你也可以在嵌入式模式下运行:

import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;


AmazonDynamoDB dynamodb = DynamoDBEmbedded.create();
dynamodb.listTables();

为了使用 DynamoDBLocal,您需要遵循以下步骤。

  1. 获取直接 DynamoDBLocal 依赖项
  2. 获取本机 SQLite4Java 依赖项
  3. 设置 sqlite4java.library.path以显示本机库

1. 获取直接 DynamoDBLocal 依赖项

这是最简单的一个,您需要这个资源库,就像 给你解释的那样。

<!--Dependency:-->
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>1.11.0.1</version>
<scope></scope>
</dependency>
</dependencies>
<!--Custom repository:-->
<repositories>
<repository>
<id>dynamodb-local</id>
<name>DynamoDB Local Release Repository</name>
<url>https://s3-us-west-2.amazonaws.com/dynamodb-local/release</url>
</repository>
</repositories>

2. 获取本机 SQLite4Java 依赖项

如果不添加这些依赖项,测试将失败,并出现500个内部错误。

首先,添加这些依赖项:

<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>sqlite4java</artifactId>
<version>1.0.392</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>sqlite4java-win32-x86</artifactId>
<version>1.0.392</version>
<type>dll</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>sqlite4java-win32-x64</artifactId>
<version>1.0.392</version>
<type>dll</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>libsqlite4java-osx</artifactId>
<version>1.0.392</version>
<type>dylib</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>libsqlite4java-linux-i386</artifactId>
<version>1.0.392</version>
<type>so</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.almworks.sqlite4java</groupId>
<artifactId>libsqlite4java-linux-amd64</artifactId>
<version>1.0.392</version>
<type>so</type>
<scope>test</scope>
</dependency>

然后,添加这个插件来获取特定文件夹的本机依赖项:

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy</id>
<phase>test-compile</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeScope>test</includeScope>
<includeTypes>so,dll,dylib</includeTypes>
<outputDirectory>${project.basedir}/native-libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

3. 将 sqlite4java.library.path设置为显示本机库

作为最后一步,您需要将 sqlite4java.library.path系统属性设置为本机库目录。在创建本地服务器之前这样做是可以的。

System.setProperty("sqlite4java.library.path", "native-libs");

完成这些步骤后,您可以根据需要使用 DynamoDBLocal。下面是为此创建本地服务器的 Junit 规则。

import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
import org.junit.rules.ExternalResource;


import java.io.IOException;
import java.net.ServerSocket;


/**
* Creates a local DynamoDB instance for testing.
*/
public class LocalDynamoDBCreationRule extends ExternalResource {


private DynamoDBProxyServer server;
private AmazonDynamoDB amazonDynamoDB;


public LocalDynamoDBCreationRule() {
// This one should be copied during test-compile time. If project's basedir does not contains a folder
// named 'native-libs' please try '$ mvn clean install' from command line first
System.setProperty("sqlite4java.library.path", "native-libs");
}


@Override
protected void before() throws Throwable {


try {
final String port = getAvailablePort();
this.server = ServerRunner.createServerFromCommandLineArgs(new String[]{"-inMemory", "-port", port});
server.start();
amazonDynamoDB = new AmazonDynamoDBClient(new BasicAWSCredentials("access", "secret"));
amazonDynamoDB.setEndpoint("http://localhost:" + port);
} catch (Exception e) {
throw new RuntimeException(e);
}
}


@Override
protected void after() {


if (server == null) {
return;
}


try {
server.stop();
} catch (Exception e) {
throw new RuntimeException(e);
}
}


public AmazonDynamoDB getAmazonDynamoDB() {
return amazonDynamoDB;
}


private String getAvailablePort() {
try (final ServerSocket serverSocket = new ServerSocket(0)) {
return String.valueOf(serverSocket.getLocalPort());
} catch (IOException e) {
throw new RuntimeException("Available port was not found", e);
}
}
}

你可以像这样使用这个规则

@RunWith(JUnit4.class)
public class UserDAOImplTest {


@ClassRule
public static final LocalDynamoDBCreationRule dynamoDB = new LocalDynamoDBCreationRule();
}

DynamoDB Local 有几个 node.js 包装器。这样就可以轻松地执行单元测试,并结合任务运行程序,比如 Gulp 或 grunt。试试 Dynamodb-localhost, Dynamodb-local

这是 bhdrkn 对 Gradle 用户的回答(他的回答基于 Maven)。还是那三个步骤:

  1. 获取直接 DynamoDBLocal 依赖项
  2. 获取本机 SQLite4Java 依赖项
  3. 设置 sqlite4java.library. path 以显示本机库

1. 获取直接 DynamoDBLocal 依赖项

添加到 build.gradle 文件的依赖项部分..。

dependencies {
testCompile "com.amazonaws:DynamoDBLocal:1.+"
}

2. 获取本机 SQLite4Java 依赖项

Sqlite4java 库已经作为 DynamoDBLocal 的依赖项下载,但是需要将库文件复制到正确的位置。添加到 build.gradle 文件中..。

task copyNativeDeps(type: Copy) {
from(configurations.compile + configurations.testCompile) {
include '*.dll'
include '*.dylib'
include '*.so'
}
into 'build/libs'
}

3. 设置 sqlite4java.library. path 以显示本机库

我们需要告诉 Gradle 运行 copyNativeDeps进行测试,并告诉 sqlite4java 在哪里可以找到文件。添加到 build.gradle 文件中..。

test {
dependsOn copyNativeDeps
systemProperty "java.library.path", 'build/libs'
}

我已经将上面的答案封装到两个 JUnit 规则中,这两个 JUnit 规则不需要修改构建脚本,因为规则可以处理本机库内容。我这样做是因为我发现 Idea 不喜欢 Gradle/玛文解决方案,因为它只是走开,做自己的事情。

这意味着步骤如下:

  • 获取 AssortmentOfJUnitRules 版本1.5.32或更高版本的依赖项
  • 获取直接 DynamoDBLocal 依赖项
  • 将 LocalDynamoDbRule 或 HttpDynamoDbRule 添加到 JUnit 测试中。

美芬:

<!--Dependency:-->
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>DynamoDBLocal</artifactId>
<version>1.11.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.mlk</groupId>
<artifactId>assortmentofjunitrules</artifactId>
<version>1.5.36</version>
<scope>test</scope>
</dependency>
</dependencies>
<!--Custom repository:-->
<repositories>
<repository>
<id>dynamodb-local</id>
<name>DynamoDB Local Release Repository</name>
<url>https://s3-us-west-2.amazonaws.com/dynamodb-local/release</url>
</repository>
</repositories>

格拉德尔:

repositories {
mavenCentral()


maven {
url = "https://s3-us-west-2.amazonaws.com/dynamodb-local/release"
}
}


dependencies {
testCompile "com.github.mlk:assortmentofjunitrules:1.5.36"
testCompile "com.amazonaws:DynamoDBLocal:1.+"
}

密码:

public class LocalDynamoDbRuleTest {
@Rule
public LocalDynamoDbRule ddb = new LocalDynamoDbRule();


@Test
public void test() {
doDynamoStuff(ddb.getClient());
}
}

我发现,亚马逊的回购作为没有索引文件,所以似乎没有功能的方式,让你把它像这样:

maven {
url = "https://s3-us-west-2.amazonaws.com/dynamodb-local/release"
}

加载依赖项的唯一方法是以 jar 形式下载 DynamoDbLocal,然后像下面这样将其放入构建脚本:

dependencies {
...
runtime files('libs/DynamoDBLocal.jar')
...
}

当然,这意味着所有的 SQLite 和 Jetty 依赖项都需要手动引入-我仍然在努力做到这一点。如果有人知道 DynamoDbLocal 的可靠回购,我真的很想知道。

2018年8月,新的 亚马逊宣布 码头形象搭载了 Amazon DynamoDB Local。它不需要下载和运行任何 JAR,也不需要添加使用第三方特定于 OS 的二进制文件(我说的是 sqlite4java)。

它就像在测试前启动 Docker 容器一样简单:

docker run -p 8000:8000 amazon/dynamodb-local

您可以为本地开发手动完成,如上所述,也可以在 CI 管道中使用它。许多 CI 服务提供了在管道期间启动其他容器的能力,这些容器可以为您的测试提供依赖性。下面是 Gitlab CI/CD 的一个例子:

test:
stage: test
image: openjdk:8-alpine
services:
- name: amazon/dynamodb-local
alias: dynamodb-local
script:
- DYNAMODB_LOCAL_URL=http://dynamodb-local:8000 ./gradlew clean test

或者比特桶管道:

definitions:
services:
dynamodb-local:
image: amazon/dynamodb-local
…
step:
name: test
image:
name: openjdk:8-alpine
services:
- dynamodb-local
script:
- DYNAMODB_LOCAL_URL=http://localhost:8000 ./gradlew clean test

诸如此类。其思想是将在 其他 答案中看到的所有配置移出构建工具,并在外部提供依赖项。可以把它看作是依赖注入/IoC,但是对于整个服务而言,不仅仅是一个 bean。

启动容器后,您可以创建一个指向它的客户端:

private AmazonDynamoDB createAmazonDynamoDB(final DynamoDBLocal configuration) {
return AmazonDynamoDBClientBuilder
.standard()
.withEndpointConfiguration(
new AwsClientBuilder.EndpointConfiguration(
"http://localhost:8000",
Regions.US_EAST_1.getName()
)
)
.withCredentials(
new AWSStaticCredentialsProvider(
// DynamoDB Local works with any non-null credentials
new BasicAWSCredentials("", "")
)
)
.build();
}

现在回到最初的问题:

您必须在测试运行之前以某种方式启动服务器

您可以手动启动它,或者为它准备一个开发人员的脚本。IDE 通常提供一种在执行任务之前运行任意命令的方法,因此您可以使用 制作 IDE来启动容器。我认为在这种情况下,在本地运行一些东西不应该是最高优先级,相反,您应该关注于配置 CI,并让开发人员根据自己的舒适程度启动容器。

服务器在每次测试之前没有启动和停止,因此测试变得相互依赖,除非在每次测试之后添加代码来删除所有表等

这是真的,但是... 你不应该开始和停止这种重量级的事情 并在每次测试前后重新创建表格。DB 测试几乎总是相互依赖的,这对它们来说没有问题。只需对每个测试用例使用唯一的值(例如,设置项的散列键来标记您正在处理的 id/特定的测试用例 id)。至于种子数据,我建议将其从构建工具和测试代码中移除。要么使用所需的所有数据创建自己的映像,要么使用 AWS CLI 创建表并插入数据。遵循单一责任原则和依赖注入原则: 你的测试代码只能做测试。所有的环境(本例中应该为它们提供表和数据)。在测试中创建表是错误的,因为在现实生活中该表已经存在(当然,除非您正在测试实际创建表的方法)。

所有开发人员都需要安装它

Docker 应该是每个开发人员在2018年必须的,所以这不是一个问题。


如果您正在使用 JUnit 5,那么使用 DynamoDB 本地扩展将客户端注入到您的测试中可能是一个好主意(是的,我正在进行自我推销) :

  1. me.madhead.aws-junit5:dynamo-v1上添加依赖项

    Xml :

    <dependency>
    <groupId>me.madhead.aws-junit5</groupId>
    <artifactId>dynamo-v1</artifactId>
    <version>6.0.1</version>
    <scope>test</scope>
    </dependency>
    

    建造,分级

    dependencies {
    testImplementation("me.madhead.aws-junit5:dynamo-v1:6.0.1")
    }
    
  2. 在测试中使用扩展:

    @ExtendWith(DynamoDBLocalExtension.class)
    class MultipleInjectionsTest {
    @DynamoDBLocal(
    url = "http://dynamodb-local-1:8000"
    )
    private AmazonDynamoDB first;
    
    
    @DynamoDBLocal(
    urlEnvironmentVariable = "DYNAMODB_LOCAL_URL"
    )
    private AmazonDynamoDB second;
    
    
    @Test
    void test() {
    first.listTables();
    second.listTables();
    }
    }
    

您还可以使用这个轻量级测试容器‘ Dynalite’

Https://www.testcontainers.org/modules/databases/dynalite/

来自测试容器:

Dynalite 是 DynamoDB 的克隆,支持本地测试 而且跑得很快。

最短的 解决方案,如果它是一个用 gradle 构建的 java/kotlin 项目,则对 sqlite4java.SQLiteException UnsatisfiedLinkError进行修复(更改后的 $PATH没有所需要的)。

repositories {
// ... other dependencies
maven { url 'https://s3-us-west-2.amazonaws.com/dynamodb-local/release' }
}


dependencies {
testImplementation("com.amazonaws:DynamoDBLocal:1.13.6")
}


import org.gradle.internal.os.OperatingSystem
test {
doFirst {
// Fix for: UnsatisfiedLinkError -> provide a valid native lib path
String nativePrefix = OperatingSystem.current().nativePrefix
File nativeLib = sourceSets.test.runtimeClasspath.files.find {it.name.startsWith("libsqlite4java") && it.name.contains(nativePrefix) } as File
systemProperty "sqlite4java.library.path", nativeLib.parent
}
}

在测试类中的简单使用(src/test) :

private lateinit var db: AmazonDynamoDBLocal


@BeforeAll
fun runDb() { db = DynamoDBEmbedded.create() }


@AfterAll
fun shutdownDb() { db.shutdown() }

试试 风暴测试!它提供了 JUnit4规则和 JUnit5扩展。它还支持 AWS SDK v1和 SDK v2。

Tempest 提供了一个用于测试 DynamoDB 客户端的库 使用 DynamoDBLocal 。它有两个实现:

  • JVM: 这是首选选项,运行 sqlite4java支持的 DynamoDBProxyServer, 在 大多数平台上有售。
  • Docker: 这在 Docker 中运行 Dynamodb-local 集装箱。

特征矩阵:

特写 Empest-test-jvm 风暴测试码头
启动时间 ~ 1s 10岁左右
内存使用情况 少一点 更多
依赖性 Sqlite4java 本机库 多克

要使用 tempest-testing,首先将此库作为测试依赖项添加:

对于 AWS SDK 1.x:

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

对于 AWS SDK2.x:

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

然后在用 @org.junit.jupiter.api.Test注释的测试中,您可以添加 TestDynamoDb作为测试 扩展名 它跨测试共享服务器,并保持其运行直到进程退出 还为您管理测试表,在每次测试之前重新创建它们。

class MyTest {
@RegisterExtension
TestDynamoDb db = new TestDynamoDb.Builder(JvmDynamoDbServer.Factory.INSTANCE) // or DockerDynamoDbServer
// `MusicItem` is annotated with `@DynamoDBTable`. Tempest recreates this table before each test.
.addTable(TestTable.create(MusicItem.TABLE_NAME, MusicItem.class))
.build();


@Test
public void test() {
PutItemRequest request = // ...;
// Talk to the local DynamoDB.
db.dynamoDb().putItem(request);
}


}

看来应该有更简单的办法。毕竟,DynamoDB Local 只是 Java 代码。难道我不能以某种方式要求 JVM 分叉自身并查看内部资源以构建类路径吗?

您可以按照这些方法做一些事情,但是要简单得多: 以编程方式搜索类路径以查找本机库的位置,然后在启动 DynamoDB 之前设置 sqlite4java.library.path属性。这是在 风暴测试这个答案((此处代码)中实现的方法,这就是为什么它们只作为纯库/类路径依赖而工作,仅此而已。

在我的案例中,需要访问 JUnit 扩展之外的 DynamoDB,但是我仍然希望库代码中包含一些自包含的内容,所以我提取了它所采用的方法:

import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
import com.amazonaws.services.dynamodbv2.local.shared.access.AmazonDynamoDBLocal;
import com.google.common.collect.MoreCollectors;
import java.io.File;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.condition.OS;


...


public AmazonDynamoDBLocal embeddedDynamoDb() {
final OS os = Stream.of(OS.values()).filter(OS::isCurrentOs)
.collect(MoreCollectors.onlyElement());
final String prefix;
switch (os) {
case LINUX:
prefix = "libsqlite4java-linux-amd64-";
break;
case MAC:
prefix = "libsqlite4java-osx-";
break;
case WINDOWS:
prefix = "sqlite4java-win32-x64-";
break;
default:
throw new UnsupportedOperationException(os.toString());
}
  

System.setProperty("sqlite4java.library.path",
Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator))
.stream()
.map(File::new)
.filter(file -> file.getName().startsWith(prefix))
.collect(MoreCollectors.onlyElement())
.getParent());
return DynamoDBEmbedded.create();
}

没有机会在很多平台上进行测试,因此错误处理可能会得到改进。

遗憾的是 AWS 没有花时间使库更友好,因为这可以在库代码本身中轻松实现。

DynamoDB Gradle 依赖项 已经包括了 SQLite 库。您可以很容易地指示 Java 运行时在 Gradle 构建脚本中使用它。以我的 build.gradle.kts为例:

import org.apache.tools.ant.taskdefs.condition.Os


plugins {
application
}


repositories {
mavenCentral()
maven {
url = uri("https://s3-us-west-2.amazonaws.com/dynamodb-local/release")
}
}


dependencies {
implementation("com.amazonaws:DynamoDBLocal:[1.12,2.0)")
}


fun getSqlitePath(): String? {
val dirName = when {
Os.isFamily(Os.FAMILY_MAC) -> "libsqlite4java-osx"
Os.isFamily(Os.FAMILY_UNIX) -> "libsqlite4java-linux-amd64"
Os.isFamily(Os.FAMILY_WINDOWS) -> "sqlite4java-win32-x64"
else -> throw kotlin.Exception("DynamoDB emulator cannot run on this platform")
}
return project.configurations.runtimeClasspath.get().find { it.name.contains(dirName) }?.parent
}


application {
mainClass.set("com.amazonaws.services.dynamodbv2.local.main.ServerRunner")
applicationDefaultJvmArgs = listOf("-Djava.library.path=${getSqlitePath()}")
}


tasks.named<JavaExec>("run") {
args("-inMemory")
}