[英]Easier DynamoDB local testing
我正在使用本地 DynamoDB进行单元测试。 这还不错,但有一些缺点。 具体来说:
我想要做的是将 DynamoDB 本地 jar 和它所依赖的另一个 jars 放在我的test/resources
目录中(我正在用 Java 编写)。 然后在每次测试之前我会启动它,使用-inMemory
运行,在测试之后我会停止它。 这样一来,任何下载 git 存储库的人都可以获得运行测试所需的所有内容的副本,并且每个测试都独立于其他测试。
我找到了一种方法来完成这项工作,但它很丑陋,所以我正在寻找替代方案。 我的解决方案是将 DynamoDB 本地文件的 .zip 文件放在test/resources
中,然后在@Before
方法中,我将其解压缩到某个临时目录并启动一个新的 java 进程来执行它。 这行得通,但它很丑陋并且有一些缺点:
$PATH
中都需要 java 可执行文件 似乎应该有一个更简单的方法。 毕竟,DynamoDB Local 只是 Java 代码。 我不能以某种方式要求 JVM 分叉自身并查看资源以构建类路径吗? 或者,更好的是,我不能只从其他线程调用 DynamoDB Local 的main
方法,这样这一切都发生在一个进程中吗? 有任何想法吗?
PS:我知道 Alternator,但它似乎还有其他缺点,所以如果我能让它工作,我倾向于坚持使用 Amazon 支持的解决方案。
要使用 DynamoDBLocal,您需要执行以下步骤。
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. 获取 Native 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
系统属性设置为 native-libs 目录。 在创建本地服务器之前这样做是可以的。
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 作为 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();
这是 bhdrkn 对 Gradle 用户的回答的重述(他的回答基于 Maven)。 还是一样的三步:
- 获取直接的 DynamoDBLocal 依赖
- 获取本机 SQLite4Java 依赖项
- 设置 sqlite4java.library.path 以显示本机库
添加到 build.gradle 文件的依赖项部分...
dependencies {
testCompile "com.amazonaws:DynamoDBLocal:1.+"
}
sqlite4java 库已作为 DynamoDBLocal 的依赖项下载,但需要将库文件复制到正确的位置。 添加到您的 build.gradle 文件...
task copyNativeDeps(type: Copy) {
from(configurations.compile + configurations.testCompile) {
include '*.dll'
include '*.dylib'
include '*.so'
}
into 'build/libs'
}
我们需要告诉 Gradle 运行copyNativeDeps
进行测试,并告诉 sqlite4java 在哪里可以找到这些文件。 添加到您的 build.gradle 文件...
test {
dependsOn copyNativeDeps
systemProperty "java.library.path", 'build/libs'
}
2018 年 8 月, 亚马逊发布了带有 Amazon DynamoDB Local 的新Docker 镜像。 它不需要下载和运行任何 JAR,也不需要使用第三方操作系统特定的二进制文件(我说的是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
或 Bitbucket 管道:
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,让开发人员启动容器,因为它对他们来说很舒服。
服务器不会在每次测试之前启动和停止,因此测试变得相互依赖,除非您在每次测试后添加代码以删除所有表等
没错,但是……您不应该在每次测试之前/之后开始和停止此类重量级的事情并重新创建表。 数据库测试几乎总是相互依赖的,这对他们来说没问题。 只需为每个测试用例使用唯一的值(例如,将项目的哈希键设置为您正在处理的票证 ID/特定测试用例 ID)。 至于种子数据,我建议也将其从构建工具和测试代码中移出。 要么使用您需要的所有数据制作您自己的图像,要么使用 AWS CLI 创建表并插入数据。 遵循单一职责原则和依赖注入原则:你的测试代码除了测试什么都不做。 所有环境(在这种情况下应为它们提供表和数据)。 在测试中创建表是错误的,因为在现实生活中该表已经存在(当然,除非您正在测试实际创建表的方法)。
所有开发人员都需要安装它
Docker 应该是 2018 年每个开发者的必备品,所以这不是问题。
如果您使用的是 JUnit 5,最好使用DynamoDB Local 扩展来在您的测试中注入客户端(是的,我正在做自我宣传):
将JCenter存储库添加到您的构建中。
pom.xml :
<repositories> <repository> <snapshots> <enabled>false</enabled> </snapshots> <id>central</id> <name>bintray</name> <url>https://jcenter.bintray.com</url> </repository> </repositories>
构建.gradle
repositories { jcenter() }
添加对by.dev.madhead.aws-junit5:dynamodb-v1
的依赖
pom.xml :
<dependency> <groupId>by.dev.madhead.aws-junit5</groupId> <artifactId>dynamodb-v1</artifactId> <version>1.0.0</version> <scope>test</scope> </dependency>
构建.gradle
dependencies { testImplementation("by.dev.madhead.aws-junit5:dynamodb-v1:1.0.0") }
在您的测试中使用扩展:
@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(); } }
我将上面的答案包装到两个JUnit 规则中,这些规则不需要更改构建脚本,因为这些规则处理本机库的内容。 我这样做是因为我发现 Idea 不喜欢 Gradle/Maven 解决方案,因为它刚刚开始并做自己的事情。
这意味着步骤是:
马文:
<!--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());
}
}
似乎应该有一个更简单的方法。 毕竟,DynamoDB Local 只是 Java 代码。 我不能以某种方式要求 JVM 分叉自身并查看资源以构建类路径吗?
您可以按照这些思路做一些事情,但要简单得多:以编程方式在类路径中搜索本机库的位置,然后在启动 DynamoDB 之前设置sqlite4java.library.path
属性。 这是在tempest-testing以及这个答案( 代码在这里)中实现的方法,这就是为什么它们只是作为纯库/类路径依赖项工作,仅此而已。
在我的例子中,需要在 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 没有花时间使库更友好,因为这可以在库代码本身中轻松完成。
对于工作中的单元测试,我使用 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;
}
});
不确定这是否是您要找的,但我们就是这样做的。
尝试风暴测试! 它提供了一个 JUnit4 规则和一个 JUnit5 扩展。 它还支持 AWS SDK v1 和 SDK v2。
Tempest 提供了一个使用DynamoDBLocal测试 DynamoDB 客户端的库。 它有两个实现:
DynamoDBProxyServer
支持的sqlite4java
,可在大多数平台上使用。特征矩阵:
特征 | 暴风雨测试jvm | 暴风雨测试码头工人 |
---|---|---|
启动时间 | ~1s | ~10s |
内存使用情况 | 较少的 | 更多的 |
依赖 | sqlite4java 原生库 | 码头工人 |
要使用tempest-testing
,首先将此库添加为测试依赖项:
对于 AWS 开发工具包 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 开发工具包 2.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
作为测试扩展。 这个扩展启动了一个 DynamoDB 服务器。 它跨测试共享服务器并保持它运行直到进程退出。 它还为您管理测试表,在每次测试之前重新创建它们。
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 有几个 node.js 包装器。 这些允许与任务运行器(如 gulp 或 grunt)结合轻松执行单元测试。 尝试dynamodb-localhost , dynamodb-local
在Hadoop中,我们还使用DynamoDBLocal进行测试和调试工作。 请参阅以下示例: https : //github.com/apache/hadoop/blob/HADOOP-13345/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/ S3A / s3guard / TestDynamoDBMetadataStore.java#L113
我发现 amazon repo 没有索引文件,因此似乎无法以允许您将其引入的方式运行:
maven {
url = "https://s3-us-west-2.amazonaws.com/dynamodb-local/release"
}
我可以加载依赖项的唯一方法是将 DynamoDbLocal 作为 jar 下载并将其放入我的构建脚本中,如下所示:
dependencies {
...
runtime files('libs/DynamoDBLocal.jar')
...
}
当然,这意味着所有 SQLite 和 Jetty 依赖项都需要手动引入 - 我仍在努力解决这个问题。 如果有人知道 DynamoDbLocal 的可靠存储库,我真的很想知道。
你也可以使用这个轻量级的测试容器“Dynalite”
https://www.testcontainers.org/modules/databases/dynalite/
从测试容器:
Dynalite 是 DynamoDB 的克隆,支持本地测试。 运行起来轻便快捷。
与修为最短的解决方案sqlite4java.SQLiteException UnsatisfiedLinkError
如果是带内置的gradle一个Java /科特林项目(一个改变$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() }
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")
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.