🙄

Testcontainers for NodeJSを試してみた

2024/01/13に公開2

https://www.docker.com/blog/8-top-docker-tips-tricks-for-2024/ を見てて、Testcontainersを知らなかったので試してみました。

前回 のコードを使ってテストしてみます。

Testcontainersとは

テストするときにdockerを使うことがあるかと思いますが、コード上でdockerの操作(起動、停止など)ができるようになります。

https://testcontainers.com/
https://testcontainers.com/getting-started/
https://node.testcontainers.org/

流れ

  • jestやtestcontainersをインストール
  • テストコード作成
  • テスト実行

実装

パッケージインストール

npm install testcontainers jest ts-jest @types/jest --save-dev

jestの準備

npx ts-jest config:init
jest.config.js
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  // ※1 timeout時間を延ばす
  testTimeout: 30000,
  // テストファイルが存在するディレクトリ
  roots: ['src/tests'],
  // テストファイルのパターン(例: *.test.js)
  testMatch: [
    '**/__tests__/**/*.ts',
    '**/?(*.)+(spec|test).ts'
  ],
};
package.json
   "scripts": {
	  ...
      "test": "jest --verbose --coverage --detectOpenHandles" 
   }

テストコードを書く

mkdir src/tests
src/tests/user.test.ts
import { DockerComposeEnvironment } from "testcontainers";
import { DataSource } from "typeorm";
import { User } from "../entity/User";
import { AppDataSource } from "../data-source";

describe('User Entity Tests', () => {
    let dataSource: DataSource;
    let container: any;
    const composeFilePath = "./";
    const composeFile = "docker-compose.yml";

    // テスト前にコンテナをセットアップ
    beforeAll(async () => {
        container = await new DockerComposeEnvironment(composeFilePath, composeFile).up();
        dataSource = await AppDataSource.initialize();
    });

    // テスト後にコンテナをクリーンアップ
    afterAll(async () => {
        await dataSource.destroy();
        await container.down();
    });

    test('Insert and retrieve a user', async () => {
        const user = new User();
        user.firstName = "Timber";
        user.lastName = "Saw";
        user.age = 25;
        await dataSource.manager.save(user);

        const users = await dataSource.manager.find(User);
        expect(users.length).toBe(1);
        expect(users[0].firstName).toBe("Timber");
        expect(users[0].lastName).toBe("Saw");
        expect(users[0].age).toBe(25);
    })
});

ここで DockerComposeEnvironmentを使ってます。理由は後述。

テスト実行

npm test

> test
> jest --verbose --coverage --detectOpenHandles

 PASS  src/tests/user.test.ts (25.959 s)
  User Entity Tests
    ✓ Insert and retrieve a user (57 ms)

-------------------------------|---------|----------|---------|---------|-------------------
File                           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------------------|---------|----------|---------|---------|-------------------
All files                      |    87.5 |      100 |   33.33 |   85.71 |
 src                           |     100 |      100 |     100 |     100 |
  data-source.ts               |     100 |      100 |     100 |     100 |
 src/entity                    |     100 |      100 |     100 |     100 |
  User.ts                      |     100 |      100 |     100 |     100 |
 src/migration                 |      50 |      100 |   33.33 |      50 |
  1704971255992-CreateUsers.ts |      50 |      100 |   33.33 |      50 | 7-11
-------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        26.161 s
Ran all test suites.

dockerで起動したmysqlを使ってテストが実行できました。

DockerComposeEnvironmentを使った理由

本当はQuickstartにあるようにGenericContainerを使おうとしたんですが、データベースに接続できない、とエラーになってしまいました。
.withExposedPorts(3306) でポートを3306で指定してるのに、実際には違うポート(MySQL is running on port 32823)で起動されてるのが原因のようです。理由は不明

run_testcontainer.ts
import { GenericContainer} from "testcontainers";
import { DataSource } from "typeorm";
import { AppDataSource } from "./src/data-source";

async function main() {
    let dataSource: DataSource;

    const container = await new GenericContainer("mysql:8.2")
        .withEnvironment({MYSQL_DATABASE : "sampledb"})
        .withEnvironment({MYSQL_USER : "example_user"})
        .withEnvironment({MYSQL_PASSWORD : "<example_user_password>"})
        .withEnvironment({MYSQL_ROOT_PASSWORD : "<root_password>"})
        .withExposedPorts(3306)
        .start();

    console.log(`MySQL is running on port ${container.getMappedPort(3306)}`);

    dataSource = await AppDataSource.initialize();

    await container.stop();
}

main();
ts-node run_testcontainer.ts

MySQL is running on port 32823 # Why?
Error: connect ECONNREFUSED 127.0.0.1:3306
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1595:16) {
  errno: -111,
  code: 'ECONNREFUSED',
  syscall: 'connect',
  address: '127.0.0.1',
  port: 3306,
  fatal: true
}

色々調べてたらdocker-compose.ymlから起動する方法があったのでこちらでやってみました。
https://node.testcontainers.org/features/compose/

参考

https://github.com/testcontainers/tc-guide-getting-started-with-testcontainers-for-nodejs/tree/main

Discussion

renadachirenadachi

.withExposedPorts(3306) でポートを3306で指定してるのに、実際には違うポート(MySQL is running on port 32823)で起動されてるのが原因のようです。

これ、正しい挙動だと思います。

withExposedPortsの引数はcontainer側のportで、ホスト側にマッピングされるport(getMappedPortで取得できるport)はtestcontainersが空いてるport探してそれを使ってると思います。

調べたらhost側のポートも指定するオプションがありました。

https://node.testcontainers.org/features/containers/#exposing-container-ports

const container = await new GenericContainer("mysql:8.2")
        .withEnvironment({MYSQL_DATABASE : "sampledb"})
        .withEnvironment({MYSQL_USER : "example_user"})
        .withEnvironment({MYSQL_PASSWORD : "<example_user_password>"})
        .withEnvironment({MYSQL_ROOT_PASSWORD : "<root_password>"})
        .withExposedPorts({ container: 3306, host: 3306 }) // これ
        .start();
tsaekitsaeki

@moeyashi さん
コメントありがとうございます。そうなんですね・・・

.withExposedPorts({ container: 3306, host: 3306 }) // これ

ありがとうございます。使う機会があったら試してみます!