Unit Testing
Zeltは、dependency injectionに対応したサービスの単体テストのためのユーティリティを含む @zeltjs/testing パッケージを提供します。
インストール
pnpm add -D @zeltjs/testing
テストランナー用Adapter
使用するテストランナー用のadapterからimportします。これにより afterAll を通じたクリーンアップが自動登録されます。
Vitest
// 理由: テストフレームワークのセットアップ例としてimportのみを示す
import { onTest, createTestTarget } from '@zeltjs/testing/vitest';
Jest
// 理由: テストフレームワークのセットアップ例としてimportのみを示す
import { onTest, createTestTarget } from '@zeltjs/testing/jest';
Bun
// 理由: テストフレームワークのセットアップ例としてimportのみを示す
import { onTest, createTestTarget } from '@zeltjs/testing/bun';
Node.js Test Runner
// 理由: テストフレームワークのセットアップ例としてimportのみを示す
import { onTest, createTestTarget } from '@zeltjs/testing/node';
Manual Setup
手動で制御したい場合や別のテストランナーを使う場合は、base packageからimportして自分で shutdownAll() を呼び出します。
// 理由: テストフレームワークのセットアップ例としてimportのみを示す
import { onTest, createTestTarget, shutdownAll } from '@zeltjs/testing';
import { afterAll } from 'your-test-runner';
afterAll(shutdownAll);
createTestTarget
createTestTarget は、dependency injectionを使ってサービスをインスタンス化する主要なテストユーティリティです。lifecycle管理とクリーンアップを自動的に処理します。
describe('UserService', () => {
it('should create user', async () => {
const { target } = await createTestTarget(UserService);
const user = await target.create({ name: 'Alice' });
expect(user.name).toBe('Alice');
});
});
Options
| オプション | 型 | 説明 |
|---|---|---|
configs | Class[] | 登録するConfiguration class |
overrides | Override[] | 依存関係のモック実装 |
戻り値
| プロパティ | 型 | 説明 |
|---|---|---|
target | T | インスタンス化されたサービス |
get | (cls) => T | コンテナから追加の依存関係を解決する |
shutdown | () => Promise<void> | クリーンアップ関数(shutdownAll に自動登録される) |
Mocking Dependencies
overrides を使って実装をモックに置き換えます(Solitary Unit Test)。
describe('UserService', () => {
it('should send welcome email', async () => {
const mockEmailService = {
send: vi.fn().mockResolvedValue(undefined),
};
const { target } = await createTestTarget(UserService, {
overrides: [
{ provide: EmailService, useValue: mockEmailService },
],
});
await target.register({ email: 'alice@example.com' });
expect(mockEmailService.send).toHaveBeenCalledWith(
'alice@example.com',
expect.stringContaining('Welcome')
);
});
});
Lifecycle Management
createTestTarget は、shutdown関数を自動的に shutdownAll へ登録します。
- Startup: テストターゲットが作成される際、登録済みの
Lifecycle実装がすべて起動します - Shutdown: テストランナーのglobal teardownで
shutdownAll()を呼び出します(adapterのimportによって自動的に処理されます)
これにより、テストが失敗した場合でもリソースが適切にクリーンアップされます。
Testing Commands
CLIコマンドをテストする際は、テストごとに新しいapp instanceを作成する必要があります。createRuntime() を呼び出した後、app instanceは再利用できません。
The Problem
グローバルなapp instanceを再利用すると、エラーが発生します。
describe('GreetCommand', () => {
it('test 1', async () => {
const nodeApp = await onNode(app);
await nodeApp.commands.execCommand(['greet']);
// 動作する
});
it('test 2 — reusing the same app instance throws', async () => {
// ❌ 既にreadyなappに対してonNode()を呼び出すことはできない
await expect(onNode(app)).rejects.toThrow(ZeltLifecycleStateError);
});
});
onNode() が呼び出されると、appは ready stateへ遷移します。同じinstanceに対して onNode() を再度呼び出すと、lifecycle hookを再登録できないため失敗します。
The Solution
テストごとに新しいapp instanceを作成します。
describe('GreetCommand', () => {
let nodeApp:
| {
shutdown(): Promise<void>;
commands: { execCommand(argv: readonly string[]): Promise<{ exitCode: number }> };
}
| undefined;
afterEach(async () => {
await nodeApp?.shutdown();
});
it('test 1', async () => {
const app = createApp([command([GreetCommand])]);
nodeApp = await onNode(app);
await nodeApp.commands.execCommand(['greet']);
// 動作する
});
it('test 2', async () => {
const app = createApp([command([GreetCommand])]);
nodeApp = await onNode(app);
await nodeApp.commands.execCommand(['greet']);
// 動作する — 新しいapp instance
});
});
Using a Factory Function
テストをすっきりさせるため、app作成をfactoryへ切り出します。
function createTestApp() {
return createApp([command([GreetCommand])]);
}
describe('GreetCommand', () => {
let nodeApp:
| {
shutdown(): Promise<void>;
commands: { execCommand(argv: readonly string[]): Promise<{ exitCode: number }> };
}
| undefined;
afterEach(async () => {
await nodeApp?.shutdown();
});
it('executes successfully', async () => {
nodeApp = await onNode(createTestApp());
const result = await nodeApp.commands.execCommand(['greet']);
// resultをassertする
});
});