Jestによるテストの基礎
describe / it / expect / jest.fn() の基本をNestJS非依存のシンプルな例で押さえる
Jestは、JavaScript / TypeScriptでよく使われるテストランナー。NestJS・React・Node.jsスクリプトを問わず使える。この記事では、NestJSの設定に入る前に、Jestの基本的な書き方を押さえる。
この記事でやること
- Jestをインストールして最初のテストを実行する
describe/it/expectの基本構造を理解するbeforeEach/afterEachでセットアップ・クリーンアップをするjest.fn()で関数をモックする- 非同期コードをテストする
- よく使うマッチャーを確認する
0. インストール
# pnpm
pnpm add -D jest @types/jest ts-jest
# npm
npm install -D jest @types/jest ts-jestpackage.json に以下を追加する。
{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "ts-jest"
}
}すでにJestがある場合(nest new で作ったプロジェクトなど)はインストール不要。
1. テストファイルの基本構造
Jestはデフォルトで *.spec.ts / *.test.ts という名前のファイルをテストとして実行する。
// src/math.spec.ts
describe("add", () => {
it("1 + 2 は 3 になる", () => {
expect(1 + 2).toBe(3);
});
it("負の数でも動く", () => {
expect(-1 + -2).toBe(-3);
});
});describe… テストをグループ化するブロック。クラス名や関数名を入れることが多いit(またはtest) … テストケースひとつ。itとtestは同じexpect… 値を検証する。toBeの部分を「マッチャー」と呼ぶ
pnpm test を実行すると、Jestはプロジェクト内の *.spec.ts と *.test.ts という名前のファイルを再帰的に探して、まとめて実行する。src/math.spec.ts もその対象になる。
pnpm testPASS src/math.spec.ts
add
✓ 1 + 2 は 3 になる (2 ms)
✓ 負の数でも動く (1 ms)2. よく使うマッチャー
// src/matchers.spec.ts
describe("マッチャー早見", () => {
it("プリミティブ値の比較", () => {
expect(1 + 1).toBe(2); // ===(完全一致)
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(true).toBeTruthy();
expect(0).toBeFalsy();
});
it("オブジェクトや配列の比較", () => {
expect({ a: 1 }).toEqual({ a: 1 }); // 深い比較(toBe は参照比較なので NG)
expect([1, 2, 3]).toContain(2);
});
it("文字列の部分一致", () => {
expect("hello world").toContain("world");
expect("hello world").toMatch(/world/);
});
it("数値の範囲", () => {
expect(3.14).toBeCloseTo(3.1, 1); // 小数点以下1桁で近似比較
expect(5).toBeGreaterThan(4);
expect(3).toBeLessThanOrEqual(3);
});
it("例外のテスト", () => {
const throwError = () => {
throw new Error("oops");
};
expect(throwError).toThrow("oops");
});
});3. beforeEach / afterEach でセットアップとクリーンアップ
テストごとに共通の初期化・後処理が必要な場合に使う。
// src/counter.spec.ts
class Counter {
private count = 0;
increment() { this.count++; }
get value() { return this.count; }
}
describe("Counter", () => {
let counter: Counter;
beforeEach(() => {
counter = new Counter(); // 各テストの前に新しいインスタンスを作る
});
it("初期値は 0", () => {
expect(counter.value).toBe(0);
});
it("increment すると 1 になる", () => {
counter.increment();
expect(counter.value).toBe(1);
});
it("2回 increment すると 2 になる", () => {
counter.increment();
counter.increment();
expect(counter.value).toBe(2);
});
});beforeEach… 各テストの前に実行afterEach… 各テストの後に実行(DBのクリーンアップなど)beforeAll…describeブロック全体の最初に1回だけ実行afterAll…describeブロック全体の最後に1回だけ実行
4. jest.fn() でモックを作る
外部依存(APIコール・DB・ファイルI/O)をテスト用の偽物に置き換えるときに使う。
// src/notifier.spec.ts
type Notifier = {
send: (message: string) => void;
};
class UserService {
constructor(private notifier: Notifier) {}
register(name: string) {
// 何かのビジネスロジック...
this.notifier.send(`${name} さんが登録しました`);
}
}
describe("UserService", () => {
it("登録時に Notifier.send が呼ばれる", () => {
const mockNotifier = { send: jest.fn() }; // 偽物の Notifier
const service = new UserService(mockNotifier);
service.register("Taro");
// 呼ばれたか
expect(mockNotifier.send).toHaveBeenCalled();
// 何を渡して呼ばれたか
expect(mockNotifier.send).toHaveBeenCalledWith("Taro さんが登録しました");
// 何回呼ばれたか
expect(mockNotifier.send).toHaveBeenCalledTimes(1);
});
});モックの戻り値を制御する
const mockFetch = jest.fn();
// 一度だけ返す値
mockFetch.mockResolvedValueOnce({ data: "hello" });
// 常に返す値
mockFetch.mockResolvedValue({ data: "always" });
// 一度だけ例外を投げる
mockFetch.mockRejectedValueOnce(new Error("network error"));テスト間でモックをリセットする
beforeEach(() => {
jest.clearAllMocks(); // 呼び出し履歴・戻り値をリセット
});5. 非同期コードをテストする
// src/async.spec.ts
async function fetchUser(id: number): Promise<{ id: number; name: string }> {
// 実際はAPIコールだが、ここではシミュレート
if (id === 0) throw new Error("user not found");
return { id, name: "Taro" };
}
describe("fetchUser", () => {
it("ユーザーを返す", async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: "Taro" });
});
it("id=0 のとき例外を投げる", async () => {
await expect(fetchUser(0)).rejects.toThrow("user not found");
});
it("resolves で成功を検証する別の書き方", async () => {
await expect(fetchUser(1)).resolves.toMatchObject({ name: "Taro" });
});
});非同期テストでは必ず async/await または return(Promiseを返す)を使う。忘れると、テストが終わる前にassertionが評価されず、常にPASSになってしまう。
6. 特定のテストだけ実行する
# ファイル名で絞り込む
pnpm test counter
# テスト名で絞り込む(-t フラグ)
pnpm test -- -t "increment"
# ファイルを --watch モードで実行
pnpm test -- --watch「テスト名」とは、it() / describe() の 第1引数の文字列 のこと。
describe("Counter", () => { // ← "Counter" が describe のテスト名
it("increment すると 1 になる", () => { // ← これが it のテスト名
...
});
});-t "increment" を指定すると、テスト名に "increment" を含む it だけが実行される。describe の名前で絞ることもでき、-t "Counter" ならその describe 内の it が全て対象になる。
it.only / describe.only でそのケース・グループだけ実行することもできる(コミット前に戻すのを忘れないこと)。
it.only("このテストだけ実行される", () => {
expect(1).toBe(1);
});7. カバレッジレポートを確認する
pnpm test -- --coverage----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
counter.ts| 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|カバレッジは「テストがコードを通過したか」であり、「ロジックが正しいか」ではない。高いカバレッジはバグがないことを保証しないが、テストが足りていない場所を発見する手がかりになる。
まとめ
| 要素 | 役割 |
|---|---|
describe | テストのグループ化 |
it / test | テストケース1件 |
expect(...).toBe(...) | 値の検証 |
beforeEach | 各テスト前の初期化 |
afterEach | 各テスト後の後処理 |
jest.fn() | 偽物の関数を作る |
mockResolvedValueOnce | モックの戻り値を設定 |
toHaveBeenCalledWith | モックの呼び出しを検証 |
次のステップ
NestJSプロジェクトでのテストは、NestJSのユニットテストとNestJS E2Eテストで扱う。