nestjs-testing-expert について
nestjs-testing-expertは、NestJSのモジュール、サービス、コントローラー、HTTPエンドポイント向けのVitestテスト作成を支援するスキルです。単体・統合・E2Eテストに加え、依存性注入、プロバイダーの差し替え、データベース選択を扱います。
主な機能
- 01SWCでNestJSのメタデータに対応したVitestを設定
- 02明示した協力オブジェクトでサービスとコントローラーをテスト
- 03モジュール構成を保ちながらプロバイダーやガードを差し替え
- 04起動済みNestアプリに対してHTTPフローをテスト
- 05単体・統合・E2Eテスト向けのデータベース方式を選択
- 06後処理、状態リセット、決定的なテストを管理
活用シーン
- 01NestJSプロジェクトにVitestを導入
- 02外部システムを置き換えてプロバイダー構成を検証
- 03認証を含むHTTPエンドポイントをE2E検証
- 04不安定または遅いテストスイートを改善
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ shipshitdev/ skills --skill nestjs-testing-expertインストール後、Claude Code で/nestjs-testing-expertを入力して読み込まれたか確認します。
同梱ファイルplugin.json
詳細
NestJS テストエキスパート
NestJS のモジュール、サービス、コントローラー、HTTP エンドポイント向けに、信頼性の高い Vitest スイートを構築します。
対象範囲
このスキルでは NestJS の仕組み、すなわちテストモジュール、プロバイダーの接続、リクエストレベルのエンドツーエンドフローを扱います。フレームワークに依存しない質問(動作をどのレベルでテストすべきか、カバレッジ数値が何を意味するか、リファクタリングに耐えるテストをどう設計するか、フレークをどう解消するか)には、testing-expert を使ってください。
使用する場面
- NestJS アプリケーションのユニットテスト、統合テスト、エンドツーエンドテストを書くとき
- テストモジュールのセットアップ、プロバイダーのオーバーライド、データベースのフェイク化を行うとき
- コントローラー、ガード、インターセプター、パイプをテストするとき
- 起動済みの Nest アプリケーションに対して HTTP エンドポイントを実行するとき
Nest アプリケーションにおけるテストレベル
- ユニット — サービスまたはプロバイダーを単独でテストし、協調する依存先はテストダブルに置き換えます。テスト対象のプロバイダーを超えるモジュールグラフは必要ありません。
- 統合 — 実際のプロバイダーを使ってモジュールをコンパイルし、外部システムはフェイク化します。レイヤー間の接続とコントラクトを検証します。
- エンドツーエンド — アプリケーションを起動し、HTTP 経由で操作します。パイプ、ガード、コントローラー、サービス、永続化に至るリクエスト全体の流れを検証します。
NestJS 向け Vitest のセットアップ
Nest の依存性注入はデコレーターのメタデータ(emitDecoratorMetadata)を読み取ります。Vitest は esbuild で変換しますが、esbuild はこのメタデータを出力しないため、コンストラクター注入は気付かないうちに undefined に解決されます。デコレーターのメタデータを出力する unplugin-swc でコンパイルしてください。
bun add -D vitest unplugin-swc @swc/core @golevelup/ts-vitest supertest @types/supertest
// vitest.config.ts — unit and integration specs
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
environment: 'node',
include: ['src/**/*.spec.ts'],
testTimeout: 10_000,
},
plugins: [
swc.vite({
module: { type: 'es6' },
}),
],
});
bun run test を高速に保ち、bun run test:e2e でアプリケーションを起動できるよう、エンドツーエンドの spec は別の設定に分けてください。
// vitest.config.e2e.ts
import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
root: './',
environment: 'node',
include: ['test/**/*.e2e-spec.ts'],
testTimeout: 30_000,
},
plugins: [swc.vite({ module: { type: 'es6' } })],
});
{
"scripts": {
"test": "vitest run",
"test:e2e": "vitest run --config vitest.config.e2e.ts",
"test:cov": "vitest run --coverage"
}
}
以前のテストランナー設定のオプションは、Vitest のオプションに一対一で対応します。パスエイリアスは moduleNameMapper から resolve.alias(または vite-tsconfig-paths プラグイン)に移り、testEnvironment は test.environment になり、transform は SWC プラグインに置き換わります。また、スイートごとのタイムアウトには test.testTimeout または vi.setConfig({ testTimeout }) を使います。globals がオフの場合は、vitest から describe、it、expect、vi、ライフサイクルフックを明示的にインポートしてください。ブラウザー操作を伴うエンドツーエンドフローには Playwright を使います。ここでの Supertest は、起動済みの Nest アプリに対する HTTP テストを担当します。
プロバイダー全体を自動モックする場合、@golevelup/ts-vitest の createMock はクラスに型付けされたディープモックを返します。
import { createMock, DeepMocked } from '@golevelup/ts-vitest';
サービスの spec
テスト対象は実際のものを使い、すべての依存先は明示的に用意して、テストモジュールをコンパイルします。注入トークンは、ORM のトークンヘルパー、クラス参照、カスタムトークン定数など、連携先が提供するものを使います。
import { Test, TestingModule } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi, type Mocked } from 'vitest';
describe('UsersService', () => {
let service: UsersService;
let repository: Mocked<UsersRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: createUsersRepositoryMock() },
],
}).compile();
service = module.get(UsersService);
repository = module.get(UsersRepository);
});
afterEach(() => {
vi.resetAllMocks();
});
it('returns only the active users of the requested organization', async () => {
const expected = [{ id: '1', organization: 'org1' }];
repository.find.mockResolvedValue(expected);
const result = await service.findAll('org1');
expect(result).toEqual(expected);
expect(repository.find).toHaveBeenCalledWith({
organization: 'org1',
isDeleted: false,
});
});
});
ビジネスルールを表す場合は、依存先が受け取った引数を検証してください。上記の isDeleted: false フィルターは実装の詳細ではなく、テスト対象の動作です。
コントローラーの spec
コントローラーを登録してサービスをスタブ化し、コントローラー固有の役割、すなわち引数の抽出、処理の委譲、レスポンスの整形だけを検証します。ビジネスルールはサービスの spec で検証してください。
describe('UsersController', () => {
let controller: UsersController;
let service: Mocked<UsersService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [{ provide: UsersService, useValue: createUsersServiceMock() }],
}).compile();
controller = module.get(UsersController);
service = module.get(UsersService);
});
it('passes the organization from the request through to the service', async () => {
const expected = [{ id: '1', email: '[email protected]' }];
service.findAll.mockResolvedValue(expected);
const result = await controller.findAll('org1');
expect(result).toEqual(expected);
expect(service.findAll).toHaveBeenCalledWith('org1');
});
});
プロバイダーのオーバーライド
実際のモジュールをコンパイルし、実際には実行してはいけないものだけを差し替えます。
overrideProvider と overrideGuard を使うと、残りのグラフは実際のまま保たれるため、テストで引き続き接続を検証できます。
const module: TestingModule = await Test.createTestingModule({
imports: [UsersModule],
})
.overrideProvider(MailerService)
.useValue(mailerMock)
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.compile();
ほかのことをテストする場合に限り、ガードをオーバーライドしてください。認可そのものについては、実際のガードを実行するエンドツーエンドテストが必要です。
統合テスト
アプリケーションを起動し、破棄可能なデータベースに対して実際の永続化を行い、テストごとに状態をリセットします。
describe('Users (integration)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(async () => {
await resetDatabase();
});
it('persists a created user and returns it on read', async () => {
const created = await request(app.getHttpServer())
.post('/api/users')
.send({ email: '[email protected]', name: 'Test' })
.expect(201);
const read = await request(app.getHttpServer())
.get(`/api/users/${created.body.id}`)
.expect(200);
expect(read.body.email).toBe('[email protected]');
});
});
afterAll でアプリケーションを閉じてください。Nest アプリケーションが解放されずに残ると、接続プールが開いたままとなり、Vitest の実行が終了しません。
エンドツーエンドテスト
同じように起動し、認証も含めて実際のクライアントとして操作します。
describe('Users API (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
authToken = await signInTestUser(app);
});
afterAll(async () => {
await app.close();
});
it('rejects an unauthenticated list request', () =>
request(app.getHttpServer()).get('/api/users').expect(401));
it('returns the caller organization users when authenticated', () =>
request(app.getHttpServer())
.get('/api/users')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
}));
});
共有の signInTestUser ヘルパーを使うと、トークンのフローを一か所にまとめられ、各 spec に記述せずに済みます。
データベース戦略
| アプローチ | 適している用途 | コスト |
|---|---|---|
| リポジトリのテストダブル | 単体テスト | 最速です。SQL やスキーマを使用しないことを確認できます |
| インメモリまたは組み込みエンジン | 統合テスト | 高速ですが、本番環境と動作が異なる場合があります |
| 実行ごとに使い捨てコンテナを使用 | 統合テストと E2E テスト | 最も遅いですが、本番環境との忠実度が最も高くなります |
スキーマを再作成するのではなく、切り詰めるかトランザクションをロールバックしてテスト間の状態をリセットしてください。スキーマの再構築は、テストスイートの実行時間の大部分を占めます。
ヒント
- 各テストで独自のモジュールをコンパイルしてください。共有すると、テスト間で状態が漏れます。
afterEachでモックをリセットし、あるテストで設定したスタブが次のテストで使われないようにしてください。describeブロックごとにアプリケーションを一度起動し、テストごとにデータをリセットしてください。テストごとの起動は、Nest のテストスイートが遅くなる一般的な原因です。- テストの決定性を保ってください。実際のタイムゾーンを前提にアサーションするのではなく、時刻を固定し(
vi.useFakeTimers()とvi.setSystemTimeを使用)、タイムゾーンを指定してください。 - テストで共有オブジェクトに対して
vi.spyOnを使用した場合は、afterEachでvi.restoreAllMocks()を使ってスパイを復元してください。
チェックリスト
- 明確な準備・実行・検証の構成
- テスト対象は実物、依存先は明示的にする。逆にしない
- 正常系だけでなく、エラー系と認証されていない場合の経路も網羅する
- ティアダウンでアプリケーションを終了し、モックをリセットする
- 実行が速い