跳转到主内容

plane

什么是 Testcontainers

Testcontainers 是一个开源库,它允许您测试任何容器化的依赖项,例如数据库、各种云技术或消息代理。为便于使用,Testcontainers 提供了许多称为模块的预配置依赖项。

除此之外,Testcontainers 支持多种语言,您可以使用这些语言轻松编写测试,例如 Python、Go、Rust、Ruby、JavaScript、.NET、Java 等。

Testcontainers 的常见用例

得益于容器技术,您可以在无需复杂设置的情况下获得全新的、干净的实例,适用于以下用例:

  • 数据访问层集成测试
  • UI/验收测试
  • 应用程序集成测试

使用 Podman 设置 Testcontainers

开始之前,您需要安装 Podman 并在套接字监听模式下运行它。

$ podman system service --time=0 &

通过以下选项之一将 Testcontainers 运行时设置为 Podman:

  • 启用 Docker 兼容性 功能。

  • 在您的主目录中创建一个 .testcontainers.properties 文件,用于 Testcontainers 的全局配置,并将以下行添加到该配置文件中:

    • macOS

      docker.host=unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')

      配置后运行以下命令:

      $ export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock
    • Linux

      docker.host=unix://${XDG_RUNTIME_DIR}/podman/podman.sock
      注意

      Testcontainers 支持的每种语言都有不同的属性受 .testcontainers.properties 文件支持。

可选: 如果您以无根模式(rootless mode)运行 Podman,则必须通过定义以下环境变量来禁用 Ryuk:

$ export TESTCONTAINERS_RYUK_DISABLED=true

创建项目

本示例使用 Testcontainers 的 Redis 服务和 Redis 模块。您可以按照以下步骤创建一个项目并安装所有依赖项。

  1. 初始化一个项目。

    $ npm init -y
  2. 安装依赖项。

    $ npm install testcontainers vitest @testcontainers/redis redis typescript --save-dev
  3. 更新 package.json 文件。

    package.json
    ...
    "scripts": {
    "test": "vitest"
    },
    ...
  4. 使用 Redis Node.js 库创建基本的 CRUD 操作。

    index.ts
    import { createClient, RedisClientType } from 'redis';

    let redisClient: RedisClientType | undefined = undefined;

    export async function connectRedis(url: string) {
    redisClient = createClient({ url });
    await redisClient.connect();
    return redisClient;
    }

    export async function setValue(key: string, value: string): Promise<string | null> {
    if (!redisClient) {
    throw new Error('Redis client is not connected');
    }
    return await redisClient.set(key, value);
    }

    export async function getValue(key: string): Promise<string | null> {
    if (!redisClient) {
    throw new Error('Redis client is not connected');
    }
    return redisClient.get(key);
    }

    export async function deleteValue(key: string[]): Promise<number> {
    if (!redisClient) {
    throw new Error('Redis client is not connected');
    }
    return await redisClient.del(key);
    }

    export async function disconnectRedis() {
    if (redisClient) {
    await redisClient.quit();
    redisClient = undefined;
    }
    }
  5. 为 CRUD 操作创建基本测试。

    index.spec.ts
    import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
    import { connectRedis, deleteValue, disconnectRedis, getValue, setValue } from '.';
    import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis';
    import { Wait } from 'testcontainers';
    import { createClient } from 'redis';

    let container: StartedRedisContainer;

    beforeAll(async () => {
    container = await new RedisContainer()
    .withExposedPorts(6379)
    .withWaitStrategy(Wait.forLogMessage('Ready to accept connections'))
    .start();

    await connectRedis(`redis://:${container.getMappedPort(6379)}`);
    });

    afterAll(async () => {
    await disconnectRedis();
    });

    beforeEach(async () => {
    // Flushind DB and adding to Redis some values before each test
    const client = createClient({ url: `redis://:${container.getMappedPort(6379)}` });
    await client.connect();

    await client.flushDb();
    await client.set('preset-key', 'preset-value');
    await client.set('preset-key1', 'preset-value1');
    await client.quit();
    });

    test('set value on server', async () => {
    // Set value
    const ret = await setValue('key', 'value');
    expect(ret).toBe('OK');

    // Update value
    const ret1 = await setValue('key', 'updated-value');
    expect(ret1).toBe('OK');
    });

    test('get value from server', async () => {
    // Get preset value
    const value = await getValue('preset-key');
    expect(value).toBe('preset-value');

    // Get not existing value
    const value1 = await getValue('key');
    expect(value1).toBeNull();
    });

    test('delete value on server', async () => {
    // Delete two records in a same time
    const res = await deleteValue(['preset-key', 'preset-key1']);
    expect(res).toBe(2);

    // Delete not existing record
    const res1 = await deleteValue(['key']);
    expect(res1).toBe(0);
    });

运行测试

首次运行 Testcontainers 时,请确保使用以下命令在 DEBUG 模式下运行测试:

$ DEBUG=testcontainers* npm test

然后,您应该能看到类似下面的行:

testcontainers [DEBUG] Loading ".testcontainers.properties" file...
testcontainers [DEBUG] Loaded ".testcontainers.properties" file
testcontainers [DEBUG] Found custom configuration: dockerHost: "unix:///run/user/1000//podman/podman.sock

这些行表明 Testcontainers 找到了配置文件,并且容器是在 Podman 引擎上而不是 Docker 上创建的。

结论

本教程提供了一个基本的分步演练,使用 Testcontainers 技术通过 Podman 运行 Redis 服务器。更多示例可以在 Testcontainers 的指南中找到。如果您遇到任何问题,请随时在 Podman Desktop 的 GitHub 上提出问题。