🙄
Testcontainers for NodeJSを試してみた
https://www.docker.com/blog/8-top-docker-tips-tricks-for-2024/ を見てて、Testcontainers
を知らなかったので試してみました。
前回 のコードを使ってテストしてみます。
Testcontainersとは
テストするときにdockerを使うことがあるかと思いますが、コード上でdockerの操作(起動、停止など)ができるようになります。
流れ
- 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から起動する方法があったのでこちらでやってみました。
参考
Discussion
これ、正しい挙動だと思います。
withExposedPortsの引数はcontainer側のportで、ホスト側にマッピングされるport(
getMappedPort
で取得できるport)はtestcontainersが空いてるport探してそれを使ってると思います。調べたらhost側のポートも指定するオプションがありました。
@moeyashi さん
コメントありがとうございます。そうなんですね・・・
ありがとうございます。使う機会があったら試してみます!