繁体   English   中英

更轻松的 DynamoDB 本地测试

[英]Easier DynamoDB local testing

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

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

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

我找到了一种方法来完成这项工作,但它很丑陋,所以我正在寻找替代方案。 我的解决方案是将 DynamoDB 本地文件的 .zip 文件放在test/resources中,然后在@Before方法中,我将其解压缩到某个临时目录并启动一个新的 java 进程来执行它。 这行得通,但它很丑陋并且有一些缺点:

  • 每个人的$PATH中都需要 java 可执行文件
  • 我必须解压一个 zip 到本地磁盘。 使用本地磁盘进行测试通常很冒险,尤其是在连续构建等情况下。
  • 我必须生成一个进程并等待它为每个单元测试启动,然后在每次测试后终止该进程。 除了速度慢之外,遗留流程的可能性似乎也很难看。

似乎应该有一个更简单的方法。 毕竟,DynamoDB Local 只是 Java 代码。 我不能以某种方式要求 JVM 分叉自身并查看资源以构建类路径吗? 或者,更好的是,我不能只从其他线程调用 DynamoDB Local 的main方法,这样这一切都发生在一个进程中吗? 有任何想法吗?

PS:我知道 Alternator,但它似乎还有其他缺点,所以如果我能让它工作,我倾向于坚持使用 Amazon 支持的解决方案。

要使用 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. 获取 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)。 还是一样的三步:

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

1. 获取直接的 DynamoDBLocal 依赖

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

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

2. 获取 Native 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'
}

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 扩展来在您的测试中注入客户端(是的,我正在做自我宣传):

  1. 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() }
  2. 添加对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") }
  3. 在您的测试中使用扩展:

     @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 解决方案,因为它刚刚开始并做自己的事情。

这意味着步骤是:

  • 获取 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());
  }
}

似乎应该有一个更简单的方法。 毕竟,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 客户端的库。 它有两个实现:

特征矩阵:

特征 暴风雨测试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

我发现 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM