0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テスト原則のFIRSTについて深ぼってみた

Last updated at Posted at 2025-10-09

1. F (Fast) - 高速であること ⚡

概要

テストは迅速に実行できなければなりません。開発者はコードを変更するたびに何度もテストを実行するため、テストの実行時間が長いと、フィードバックサイクルが遅くなり、開発リズムが著しく損なわれます。

なぜ重要か?

テストの目的は、開発者に迅速なフィードバックを提供することです。

コーディングは「思考→実装→確認」の短いサイクルを繰り返すことでリズムが生まれる。

もしもテストの実行に数十秒〜数分もかかると、このサイクルが崩壊し、開発者は待っている間に集中力を失い、別の作業を始めやすくなります。

結果として、テストの実行頻度が下がり、「単なるコミット直前の儀式」のようになってしまいます。高速なテストは、思考の速度を妨げないために不可欠です。

どう達成するか?

I/O(入出力)を徹底的に排除します。I/OはCPUの計算に比べて桁違いに低速です。

ECRSのEによる高速化が暗黙的に使われます。

①. データベースI/Oの排除

本物のデータベース(ネットワーク越し)の代わりに、インメモリデータベース(SQLite, H2など)を使います。

②. ネットワークI/Oの排除

外部APIや他のマイクロサービスを実際に呼び出す代わりに、モックやスタブを使い、その振る舞いをシミュレートします。

③. ディスクI/Oの排除

ファイルの読み書きをテストする場合、実際のディスクではなく、テスト中だけ存在するインメモリファイルシステムを利用します。

アンチパターン:低速なテスト

・1つの単体テストを実行するのに数秒かかる。

・テストのたびに、本物のデータベースへのネットワーク接続や、外部APIへのHTTPリクエストが発生している。

代表例

データベースアクセスの高速化

NG:単体テストが、ネットワーク越しの本物のPostgreSQLデータベースに接続しに行く。

OK:テスト実行時のみ、インメモリデータベース(例: PythonのSQLite、JavaのH2)に切り替える。
これにより、ネットワーク遅延やディスクI/Oを完全に排除し、ミリ秒単位でDB操作をテストできます。

外部API呼び出しの排除

NG:決済機能をテストするたびに、StripeやPayPalのテスト用APIエンドポイントを実際に呼び出す。

OK:モックやスタブを使い、外部APIの振る舞いをシミュレートします。
テストは実際のHTTP通信を行わず、即座に返ってくる偽の応答(成功/失敗)を使ってロジックを検証します。

2. I (Independent) - 独立していること 🧩

概要

各テストは、他のテストから完全に独立していなければなりません。
テストが実行される順序に依存したり、他のテストが残した「ゴミ」に影響されたりしてはいけません。

なぜ重要か?

テストの信頼性とデバッグの容易性を確保するためです。

もしテストが互いに依存していると、あるテストの失敗が、そのテスト自体の問題なのか、あるいは前に実行された別のテストが残した副作用のせいなのか?という関心の分離が非常に困難になります。

また、依存関係があるとテストを並列実行できず、全体の実行時間が長くなります。

なので、独立性は、失敗の原因を即座に特定するための前提条件です。

どう達成するか?

各テストを「自分だけの世界」で完結させます。

テストにおける単一責任の原則

①. テストごとのセットアップとクリーンアップ

各テストメソッドの実行前に、
そのテストに必要なデータだけを準備し(beforeEach / setup)、実行後に必ず元の状態に戻す(afterEach / teardown)というライフサイクルを徹底します。

②. 状態を共有しない

グローバル変数や静的変数など、テスト間で共有される可能性のある状態を使わないようにします。

カプセル化を意識した設計がなされていれば、状態共有なんてしません。

③. 実行順序をランダムにする

テストランナーの設定で、毎回テストの実行順序をランダムにすることで、隠れた依存関係を早期に発見できます。

アンチパターン:依存したテスト

・test_create_userを実行した後でなければ、test_update_userが成功しない。

・全てのテストを実行すると成功するが、特定のテストだけを単体で実行すると失敗する。

代表例

テストごとのクリーンな状態の保証

NG:test_AがDBに作成したユーザーデータを、test_Bが読み込んで利用する。

OK:各テストメソッドの**実行前(setupやbeforeEach)**に、そのテストで必要なデータだけを作成し、**実行後(teardownやafterEach)**に必ずクリーンアップします。これにより、全てのテストは常に予測可能な「まっさら」な状態で開始されます。

グローバルな状態の排除

NG:あるテストが変更したグローバル変数や静的変数を、別のテストが参照している。

OK:テスト間で状態を共有しません。
必要なデータは各テストが個別に準備し、テストケース内で完結させます。

3. R (Repeatable) - 再現可能であること 🔄

概要

テストは、いつ、どこで、誰が実行しても、常に同じ結果にならなければなりません。

開発者のローカルマシン、CI/CD環境、他の同僚のマシンなど、環境の違いによって成否が変わるようではいけません。

なぜ重要か?

テストスイートが信頼できる唯一の真実(Single Source of Truth) であるために不可欠。

「自分のPCでは成功するのにCI環境では失敗する」という状況は、「動くのか動かないのか分からない」という不信感を生み、チーム全体の生産性を著しく低下させます。

再現性は、チーム全員が同じ基準で品質を判断できるようにするために必要です。

どう達成するか?

テストの実行結果に影響を与えうる外部の不確定要素を完全に制御します。

①. 時刻を制御する

DateTime.now()のような関数を直接使わず、テスト中は時刻を特定の値に固定するライブラリを使います。

②. 乱数を制御する

乱数を使うロジックをテストする場合、乱数生成器に固定のシード値を与え、常に同じ乱数列が生成されるようにします。

③. 外部環境を制御する

外部API、OSのロケール設定(日付フォーマットなど)、特定のファイルパスといった、
実行環境に依存する要素はすべてモックするか、テストコード内で明示的に設定します。

アンチパターン:再現性のないテスト

・自分のPCでは成功するのに、CIサーバーで実行すると失敗する。

・午前中に実行すると成功するが、午後になると失敗する(タイムゾーンの問題など)。

代表例

現在時刻への依存の排除

NG:テストコード内でDateTime.now()のような関数を直接使い、現在時刻に依存したロジックをテストする。

OK:テスト中は、時刻を固定するライブラリ(Time-freezing)を使ったり、時刻を外部から注入(DI)できるように設計したりします。
これにより、テストは常に「2025年10月10日 01:00:00」のような特定の時点を基準に動作します。

乱数への依存の排除

NG:乱数を使って生成されるIDなどを扱うテストが、実行のたびに結果が変わる。

OK:テスト実行時は、乱数生成器に固定のシード値を与えます。
これにより、生成される乱数列が常に同じになり、テスト結果が安定します。

4. S (Self-Validating) - 自身で成否を検証できること

概要

この原則の核心は、テストの成否を判断するために、人間による手作業や解釈を一切不要にすることです。

まさに、テストプロセスの手動成否チェックに対するECRSのEの適用ですね。

テスト結果は、テストランナーが出力する「PASS ✅」か「FAIL ❌」だけで、明確かつ自動的に判断できなければなりません。

なぜ重要か?

テストの自動化を完了させる最後のピースです。

テストの最終的な成否判断を人間に委ねるならば、それは自動化されたテストではなく、
単なる「手動テスト支援スクリプト」です。

CI/CDパイプラインが「ビルドを自動的にブロックする」といった品質ゲートとして機能するためには、テスト自身が明確なPASS/FAILを表明できなければなりません。

どう達成するか?

アサーション(Assertion) を徹底的に使います。

①. 期待値の表明

テストの最後に、結果が期待通りであることをassert文(例: assertEquals(expected, actual))でプログラム的に検証します。

②. 例外の検証

特定の条件下で例外がスローされるべき場合、assertThrowsのような構文で、期待通りの例外がスローされたことを検証します。

③. ログや出力を検証しない

コンソール出力やログファイルを目で見て確認するプロセスをなくし、テストコードがそれらの出力を読み込んで内容を検証するようにします。

アンチパターン:自己検証的でないテスト

・テストを実行すると、コンソールに大量のログが出力され、最後に「このログを目で見て、"ERROR"という文字列がないことを確認してください」と指示される。

・テストがUIを自動操作した後、「画面のスクリーンショットを見て、ボタンが緑色になっていることを確認してください」と要求される。

これらは、テストの最終判断を人間に委ねているため、自己検証的ではありません。

代表例①「契約による設計(DbC)」の検証📜

「契約による設計(DbC)」で定義された事前条件・事後条件・不変条件がコードの実行結果として満たされているかをassert文などで自動的に表明・検証するのは、Self-Validating原則の高度で優れた代表例です。

自己検証的なテストは、その契約を履行しているかをチェックする「自動監査官」の役割を果たします。

事前条件の検証

不正な引数(契約違反)を渡してメソッドを呼び出し、
期待通りの例外(例:IllegalArgumentException)がスローされることをassertで検証します。

事後条件の検証

メソッド実行後、その戻り値やオブジェクトの状態が、契約通り(期待通り)になっていることをassertで検証します。これはテストの最も一般的な形です。

不変条件の検証

ある操作の実行開始時と正常終了時に共通して常に保証されるべき状態についての条件
(例: user.balanceは決して負にならない)が保たれていることをassertで検証します。

代表例②手作業による確認の自動化(より基本的な例)

より基本的なレベルでは、Self-Validatingは以下のような手作業をなくすことを指します。

ログファイルの目視確認 → コードによる検証

NG:テストが生成したログファイルを人間が目で見て、エラーがないか確認する。

OK:テストコードがログファイルを読み込み、assert "ERROR" not in log_contentsのように、エラー文字列が含まれていないことをプログラムで検証する。

UIのスクリーンショット確認 → UIオートメーションツールによる検証

NG:テストが撮ったスクリーンショットを人間が見て、UIの崩れや色の変化を確認する。

OK:PlaywrightやSeleniumのようなツールを使い、「特定のボタンのCSSプロパティbackground-colorが#00FF00であること」をassertで検証する。

データベースの直接確認 → コードによるDB状態検証

NG:テスト実行後、開発者がSQLクライアントを立ち上げ、SELECT文を叩いてDBの状態を目で見て確認する。

OK:テストコードがDBに接続し、SELECT文を実行して結果を取得し、
assert result['status'] == 'completed'のように、期待する状態になっていることをプログラムで検証する。

5. T (Timely) - タイムリーであること ⏱️

概要

テストは、それを必要とするプロダクションコードが書かれるのと同時か、その直前に書かれるべきです。これはテスト駆動開発(TDD)の核心的な思想です。

なぜ重要か?

この原則は、テストを開発プロセスの一部として活用するためのものです。

テストを後回しにすると、それは単なる「品質保証のためのコスト」になります。

でもコードを書く直前に書くと、テストは実装の仕様を定義する設計ツールに変わります。
これにより、プロダクションコードが必要な機能だけを持つ、シンプルでテストしやすい設計になることが促進されます。

どう達成するか?

テスト駆動開発(TDD) のプラクティスを実践します。

①. テストファースト

プロダクションコードを書く前に、そのコードが満たすべき仕様を定義する、まずは失敗するテストを書きます。

②. レッド→グリーン→リファクタリング

失敗するテスト(レッド)をパスさせる最小限のコードを書き(グリーン)、その後コードをクリーンにする(リファクタリング)という短いサイクルを繰り返します。

③. バグ修正のテスト

バグが発見されたら、修正コードを書く前に、まずそのバグを再現する失敗するテストを書きます。そして、そのテストがパスするようにコードを修正します。これにより、バグの再発を恒久的に防止できます。

アンチパターン:後回しにされるテスト

・機能のプロダクションコードを全て書き上げた数週間後に、まとめてテストを書こうとする。(ガチガチのウォーターフォールV字モデル案件であるあるですね💦)

・バグが報告された際、修正コードだけを書いて、そのバグを再現するテストコードを書かない。

代表例

テスト駆動開発 (TDD) のサイクル

NG:OrderServiceの全メソッドを一度に実装し、後からテストを追加する。

OK:
①. まず「正常な注文ができること」を検証する失敗するテストを書く。

②. そのテストをパスさせる最小限のプロダクションコードを書く。

③. 次に「在庫不足で失敗すること」を検証する失敗するテストを追加する。

④. そのテストをパスさせるプロダクションコードを追加する。

このように、テストを道しるべとして開発を進めます。

バグ修正のプロセス

NG:バグを修正し、手動で動作確認して完了とする。

OK:
①. まず、報告されたバグを確実に再現する失敗するテストケースを書く。

②. そのテストが成功(パス)するようにプロダクションコードを修正する。

これにより、バグが本当に修正されたことを証明し、将来のコード変更で同じバグが再発(デグレード)しないことを保証できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?