Dev Study
← 解説「純関数のテスト — 入力と出力だけの関数は最強に試しやすい」に戻る

サンプルコードで身につける: 純関数のテスト — 入力と出力だけの関数は最強に試しやすい

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1純関数テストの基本形

純関数のテストは「呼んで、返り値を確かめる」だけです。describe で対象ごとにグループを作り、it を1ケースずつ並べます。準備も後片付けも要らないのが純関数の強みです。

// discount.ts — 割引後の価格を返す純関数
export function discounted(price: number, rate: number): number {
  return Math.floor(price * (1 - rate));
}

// discount.spec.ts
import { discounted } from './discount';

describe('discounted', () => {
  it('2割引を計算できる', () => {
    expect(discounted(1000, 0.2)).toBe(800);
  });

  it('端数は切り捨てる', () => {
    expect(discounted(999, 0.1)).toBe(899);
  });
});

2境界値を試す習慣 — バグは端っこに潜む

普通の値のテストだけでは不十分です。0件、null、極端な値といった境界を渡すと、想定外の挙動が見つかります。境界値のテストを書く過程で仕様の曖昧さにも気づけます。

import { totalPrice } from './price';

describe('totalPrice の境界値', () => {
  it('0件の配列なら0を返す', () => {
    expect(totalPrice([])).toBe(0);
  });

  it('1件だけでも正しく返す', () => {
    expect(totalPrice([500])).toBe(500);
  });

  it('大きな値でも桁あふれせず計算できる', () => {
    expect(totalPrice([1_000_000_000, 2_000_000_000])).toBe(3_000_000_000);
  });
});

// 「0件のとき」「nullが来たら」を考えること自体が仕様の確認になる

3toBe と toEqual の使い分け

数値や文字列は toBe で比較しますが、配列やオブジェクトは中身が同じでも別物と判定されてしまいます。中身の一致を見たいときは toEqual を使う、と覚えましょう。

// 数値・文字列・真偽値 → toBe(そのまま比較)
expect(1 + 2).toBe(3);
expect('こん' + 'にちは').toBe('こんにちは');

// 配列・オブジェクト → toEqual(中身を比較)
expect([1, 2, 3].map((n) => n * 2)).toEqual([2, 4, 6]);
expect({ name: '田中', age: 30 }).toEqual({ name: '田中', age: 30 });

// 注意: 配列やオブジェクトに toBe を使うと、中身が同じでも
// 「別のインスタンス」なので失敗する
// expect([1, 2]).toBe([1, 2]); // ← これは失敗する

// 文字列の部分一致には toContain も使える
expect('合計: 400円').toContain('400');

4テストしにくいロジックを純関数に切り出す

コンポーネントの中に埋まった計算は、コンポーネントごと組み立てないとテストできません。計算部分を純関数として外に出せば、関数単体でテストでき、コンポーネントも薄くなります。

// Before: 計算がコンポーネントの中に埋まっている
// → テストするには TestBed で画面ごと組み立てる必要がある
// export class Cart {
//   items = signal<Item[]>([]);
//   total = computed(() =>
//     this.items().reduce((s, i) => s + i.price * i.qty, 0));
// }

// After: 計算を純関数 cart-logic.ts に切り出す
export function cartTotal(items: { price: number; qty: number }[]): number {
  return items.reduce((s, i) => s + i.price * i.qty, 0);
}

// コンポーネントは切り出した関数を呼ぶだけの薄い形になる
// total = computed(() => cartTotal(this.items()));

// テストは関数だけで完結し、画面の組み立ては不要
it('数量を掛けて合計する', () => {
  expect(cartTotal([{ price: 100, qty: 3 }])).toBe(300);
});

5失敗したテストの出力を読む

テストの失敗は敵ではなく情報です。どの it が、期待値いくつに対して実際いくつだったのかが表示されるので、出力を落ち着いて読めばバグの位置まで一直線にたどり着けます。

# テストが失敗したときの出力例
#
#  ✗ totalPrice > 0件なら0を返す
#
#    Expected: 0        ← 期待していた値
#    Received: NaN      ← 実際に返ってきた値
#
#    at price.spec.ts:12:31
#
# 読み方:
#   1. どの it が失敗したか(0件のケース)
#   2. 期待と実際の差(0 のはずが NaN)
#   3. 場所(price.spec.ts の12行目)
# → 「空配列のとき NaN になるバグ」と原因の当たりが付く