はじめに
ソフトウェア開発において、テストは品質を保証するために欠かせないものです。しかし、すべてのテストが良いテストとは限りません。本記事では、なぜテストを行うのか、テストの質を向上させるポイントを実例を踏まえて紹介します
テストケースの考え方や、テストが書きやすいコードについてもご紹介しているので、こちらもあわせてご覧ください。
そもそもテストをなぜ行うのか?
-
バグの早期発見:開発の初期段階で問題を見つけることで、修正コストを抑える。
-
リファクタリングの安心感:既存のコードを改善する際に、動作を保証する。
-
ドキュメントとしての役割:テストコードが、関数やクラスの期待される動作を示す。
-
デプロイの安全性向上:プロダクションにリリースする前に、不具合を防ぐ。
-
開発の効率化:自動化されたテストにより、手動での確認作業を減らせる。
テストの質を向上させるポイント
良いテストの特徴として、以下のポイントが挙げられます。
-
実行可能性
- 誰でもテストを実施できる。
- 実施者に依存せず、同じ結果が得られる(再現性)。
-
汎用性
- テストが使い回せる(再利用可能)。
-
冪等性
- テストは何度実行しても同じ結果になる(結果が変わらない)。
-
KISS原則
- シンプルかつ分かりやすく保つ。
- 1つのテストは1つのことだけを検証する(単一責任の原則)。
-
網羅性
- 要件や仕様を十分にカバーし、抜け漏れがない。
- エッジケースや異常系も含めたテストがある。
-
独立性
- テストケース間に依存関係がない。
- 1つのテストが失敗しても他のテストには影響しない。
TypeScript によるテストの実例
テスト対象の関数
まず、テスト対象の calculateDiscountedPrice 関数を定義します。この関数は価格と割引率を受け取り、割引後の価格を計算します。
異常系(負の値や割引率の上限超過)についても適切にエラーハンドリングを行っています。
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% 超過)などを網羅。
- 独立したテスト
- それぞれのテストケースが他のテストに依存せず、個別に実行可能。
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つのテストに詰め込んでいる
- テストが失敗したときに原因が特定しづらい
- 例外の検証が不十分
- 正しくエラーメッセージを確認していない
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); // 具体的なエラーメッセージを確認していない
}
});
});
まとめ
良いテストを書くことで、バグを減らし、安心して開発ができるようになります。適切なテスト戦略を考えながら、プロジェクトに合ったテストを導入してみましょう!