なぜテストをするのか？テストの質を向上させるためのポイント

## はじめに

ソフトウェア開発において、テストは品質を保証するために欠かせないものです。しかし、すべてのテストが良いテストとは限りません。本記事では、なぜテストを行うのか、テストの質を向上させるポイントを実例を踏まえて紹介します

テストケースの考え方や、テストが書きやすいコードについてもご紹介しているので、こちらもあわせてご覧ください。

[テストが書きやすいコードを書く](https://qiita.com/kentosity/private/d8654dad4fbf216f6843)
[テストケースの考え方](https://qiita.com/KoutaKawaguchi1101/private/24e14c34b4ca055931c6)

## そもそもテストをなぜ行うのか？

- バグの早期発見：開発の初期段階で問題を見つけることで、修正コストを抑える。

- リファクタリングの安心感：既存のコードを改善する際に、動作を保証する。

- ドキュメントとしての役割：テストコードが、関数やクラスの期待される動作を示す。

- デプロイの安全性向上：プロダクションにリリースする前に、不具合を防ぐ。

- 開発の効率化：自動化されたテストにより、手動での確認作業を減らせる。


## テストの質を向上させるポイント

良いテストの特徴として、以下のポイントが挙げられます。

- 実行可能性
    - 誰でもテストを実施できる。
    - 実施者に依存せず、同じ結果が得られる（再現性）。

- 汎用性
    - テストが使い回せる（再利用可能）。

- 冪等性
    - テストは何度実行しても同じ結果になる（結果が変わらない）。

- KISS原則
    - シンプルかつ分かりやすく保つ。
    - 1つのテストは1つのことだけを検証する（単一責任の原則）。

- 網羅性
    - 要件や仕様を十分にカバーし、抜け漏れがない。
    - エッジケースや異常系も含めたテストがある。

- 独立性
    - テストケース間に依存関係がない。
    - 1つのテストが失敗しても他のテストには影響しない。

## TypeScript によるテストの実例
### テスト対象の関数


まず、テスト対象の calculateDiscountedPrice 関数を定義します。この関数は価格と割引率を受け取り、割引後の価格を計算します。
異常系（負の値や割引率の上限超過）についても適切にエラーハンドリングを行っています。

```typescript
function calculateDiscountedPrice(price: number, discount: number): number {
  if (price < 0 || discount < 0) {
    throw new Error("Price and discount must be non-negative");
  }
  if (discount > 100) {
    throw new Error("Discount cannot exceed 100%");
  }
  return price - (price * (discount / 100));
}
```




### 推奨されるテストの特徴





- 冪等性を満たしている
    - 同じ入力に対して常に同じ結果が得られるように設計されている。 
- 正常系・異常系のカバレッジが高い
    - 正常な入力、境界値（0% 割引, 100% 割引）、異常値（負の値, 100% 超過）などを網羅。
- 独立したテスト
    - それぞれのテストケースが他のテストに依存せず、個別に実行可能。


```typescript
describe("calculateDiscountedPrice", () => {
  it("should correctly calculate the discounted price", () => {
    expect(calculateDiscountedPrice(1000, 20)).toBe(800); // 割引適用後の正しい価格を検証
  });

  it("should return the same price when discount is 0%", () => {
    expect(calculateDiscountedPrice(500, 0)).toBe(500); // 割引がない場合
  });

  it("should return 0 when discount is 100%", () => {
    expect(calculateDiscountedPrice(1000, 100)).toBe(0); // 割引100%のケース
  });

  it("should throw an error if price is negative", () => {
    expect(() => calculateDiscountedPrice(-100, 20)).toThrow("Price and discount must be non-negative");
  });

  it("should throw an error if discount is negative", () => {
    expect(() => calculateDiscountedPrice(100, -10)).toThrow("Price and discount must be non-negative");
  });

  it("should throw an error if discount exceeds 100%", () => {
    expect(() => calculateDiscountedPrice(100, 150)).toThrow("Discount cannot exceed 100%");
  });
});

```



### 改善の余地があるテストの例

以下のテストには、いくつかの問題点があります。

- アサーションが曖昧
    - 期待する具体的な値をチェックしていない
- 複数のケースを1つのテストに詰め込んでいる
    - テストが失敗したときに原因が特定しづらい
- 例外の検証が不十分
    - 正しくエラーメッセージを確認していない

```typescript
describe("calculateDiscountedPrice", () => {
  it("should calculate something", () => {
    expect(calculateDiscountedPrice(1000, 20)).toBeTruthy(); // 曖昧なアサーション
  });

  it("should handle multiple cases in one test", () => {
    expect(calculateDiscountedPrice(1000, 20)).toBe(800);
    expect(calculateDiscountedPrice(200, 50)).toBe(100); // 1つのテストに複数の検証（単一責任の原則に違反）
  });

  it("should handle errors", () => {
    try {
      calculateDiscountedPrice(-100, 20);
    } catch (e) {
      expect(e).toBeInstanceOf(Error); // 具体的なエラーメッセージを確認していない
    }
  });
});
```

## まとめ

良いテストを書くことで、バグを減らし、安心して開発ができるようになります。適切なテスト戦略を考えながら、プロジェクトに合ったテストを導入してみましょう！