概要
プロダクト開発を行う上で、テストコードは重要な要素であるかと思います。
ユニットテストコードを書くことで、クラス単位の動作保証を行うことが出来ます。また、E2Eテストやインテグレーションテストを書くことで、DBアクセスや外部連携を含めた、プロダクトにおける一気通貫の動作を確認することが可能になります。
作成したテストコードは、CICDと組み合わせて、自動テストとして定期的に実行させます。これにより、既存のソースコードを変更した際の品質を (ある一定レベルにおいてですが) 担保することが出来るようになります。結果として、開発メンバーは積極的なリファクタリングを行えるようになり、健全な開発のライフサイクルが回る・・・という流れになります。
テストコードも、プロダクションコードと同様に、継続的に保守・開発していく必要があり、一定のお作法に則って開発していく必要があります。無秩序で設計が不十分なテストコードは、レガシーなプロダクションコードと同様に技術的な負債となり、保守・運用が困難になってしまうことでしょう。
本記事は、技術的負債とならないテストコードを書くために、私自身が考慮した方が良いと思う内容を 6つの観点 で纏めています。基本的な内容もあり、普段テストコードをバリバリ開発している方々には、常識的な内容もあるかと思いますが、ご容赦ください。
尚、本記事は C# における開発を想定しています。…と言っても、C#固有の言語仕様に縛られるような内容は少ないかと思いますので、普段別言語を扱ってテストコードを書いている方も、是非、ご意見を頂けると参考になります。
また、本記事におけるテストコードは、純粋なクラス単位のユニットテストに限らず、DBや外部連携を含むE2Eテストも想定しています。
お題目
- AAAパターンを意識する
- 再利用可能な処理は共通化する
- テストの実行時間を意識する
- テストの意図を明確にし、判りやすい名前を付ける
- テストをカテゴリ分けし、分析可能な状態にする
- 冪等性をもたせる
1. AAAパターンを意識する
テストコードを書く上で AAA(Arrange、Act、Assert)の3つのセクションで構成することを意識しましょう。
-
Arrange
テストに必要な条件を準備します。テスト対象となるオブジェクトの生成やパラメータの構築、モックオブジェクトの準備、テストデータの投入等が該当します。 -
Act
テスト対象となる振る舞いを実行します。 -
Assert
振る舞いの結果を取得し、想定通りであるかを検証します。
上記パターンを遵守することで、テストコードの記述に秩序が生まれ、テストコードの可読性向上に繋がります。
記載しているテストコードが、上記3セクションの流れ当てはまらない場合は、テストコードの妥当性を疑ってみると良いでしょう。
例として 「Act・Assertが複数回出てくる」とか 「Actの後にArrangeが登場している」という場合は、「テストの粒度が大きすぎないか」「テストで検証すべき内容が1テストケース内に多数存在するのではないか」といった具合です。
(※) C#のAAAパターンに関して、具体的なコードも含めて紹介されている記事がありました。
とても分かり易く、丁寧な解説であったため、参考としてリンクを貼らせていただきます。
速習 AAA : Arrange-Act-Assert による読みやすいテスト
2. 再利用可能な処理は共通化する
これは、通常のプロダクションコードを書く場合と全く同じ話ですが、繰り返し利用する処理は共通化しましょう。
特に、Arrange や Assert で利用する処理は、類似の処理を書く場合が多くなると思います。
- テストデータの読み込み、投入処理。
- 大掛かりなモックオブジェクトの準備処理。
- 独自クラスに対するアサーションのロジック。
共通処理をどのような粒度で作成し、プロジェクトのどこに置くかというのは匙加減が難しいところです。
テストコードの規模が小さいうちは、単純にUtilクラスに纏めたり、拡張メソッドを作成するだけでも十分だったりしますが、テストコードの規模が肥大化してきた場合は、プロダクションコードと同様にリファクタリングを行い、クラスの分割や整理を行うと良いでしょう。
どのテストでも必ず必要になる処理がある場合、例えば、DBを利用するテストでDB接続準備が必要だったり、APIの疎通テストを行うテストでテスト用APIサーバが必要だったり という場合は、テストコード用の基底クラスを設けて、それを継承するようにテスト設計を行うと良いでしょう。
また、テストパターンが類似しているテストケースについては、パラメタライズドテストを利用しましょう。
パラメタライズドテストは、テストケースの入力引数を動的に変更することで、
テストケースのロジックを再利用しつつ、テストのパターンを増やすことが出来る仕組みです。
Arrangeで準備するパラメータや、Assertで利用する期待値をパラメータにすることで、
類似したテストケースに対して、個別にテストケースを作成することなく、テストパターンを加えることが出来ます。
xUnitは[Theory]、NUnitは[TestCase]、MsTest v2だと[DataTestMethod]という感じで、
C#の代表的なテストフレームワークは、どれも利用可能です。
3. テストの実行時間を意識する
テストコードの規模が大きくなり、テスト件数が増えていくと、テスト実行にかかる時間を考慮する必要が出てきます。
仮に、1テストケース当りが3秒で終わるテストコード (実際に実行時間3秒はかなり長い訳ですが・・・) があったとして、テストケース数が 1,000件 となると全ケース実行するためには 50分 かかる計算になります。
テスト実行にかかる時間が長くなると、テストを気軽に実行することが難しくなり、最悪のケースとしては、時間がかかるからテストは実行しない、といった事になりかねないでしょう。
そのため、普段からテストコードを書く上で実行時間が短くなるように配慮しておくと良いでしょう。
- 初期化に時間がかかるオブジェクトは、テストケース (メソッド) 単位でなくテストクラス単位で初期化し、初期化回数を極力減らす。
- テストで使用するデータは可能な限り共通化し、投入回数を少なくする。
- テストデータが大量の場合は、バルク処理による時間短縮を検討する。
これらは、プロダクションコードを書く際には、当たり前に意識していることばかりかと思います。テストコードを実行する環境も、プロダクション環境と同様にリソースは限られており、それらをどうやったら効率良く使えるか…というように考えて行けば、自然と適切なコードになって行くかと思います。
4. テストの意図を明確にし、判りやすい名前を付ける
これも当たり前の事ですが、テストコードには判りやすい名前を付けましょう。
メソッド名からテストの意図が判るようにするのがベストです。C#では日本語のメソッド名も使えるので、日本語を使ってしまうというのも手かと思います。
また、テストコードとは別に、テストケースの意図をExcel等で資料化している場合 (ドキュメンテーションの意図も含めて、テストパターンを一覧表やマトリクスで整理しているケースを想定しています) は、それらがソースコードとマッピング出来るように、メソッドを命名したり、コメントに記載したりしましょう。
5. テストをカテゴリ分けし、分析可能な状態にする
別の記事 でも書かせていただきましたが、C#のテストフレームワークでは、カテゴリを付与することが出来ます。特定のカテゴリを対象としてテストを回したり、フィルタリングすることが出来るため、テストコード開発の初期段階から設定しておくと、何かと便利です。
利用例としては、以下のようなイメージです。
- ユニットテストとe2eテストを別カテゴリにして、コードコミット時にはユニットテスト、デイリーの自動テストではe2eテストを実行するようにする。
- 実行時間の長いテストケースに対してカテゴリを付与し、自動テスト実行対象から一時的に除外する。
因みに、コマンドラインでテスト実行後に、テスト結果ファイル (.trx) が生成されますが、これはVisual Studioで開くことが出来ます。 (微妙に使い辛いですが。。。)
6. 冪等性をもたせる
実行するテストコードが外部に依存せず、全てコード内で完結していれば良いのですが、多くの場合はDBや外部アクセスを行う必要があるかと思います。またその際、モックオブジェクトやインメモリDBなど、生成・破棄が容易なものが採用できず、実際のDBや外部システムを利用するシーンがあるかと思います。
そういった場合は、テストコードを繰り返し実行するケースを想定し、冪等性を持たせるようにしましょう。
- DBを利用する場合、テスト対象となるデータの初期化を適切に行い、テスト実行時の状態に依存しないようにする。
- ファイル連携を行う場合、前回テスト時のゴミが残っていないか確認し、残っている場合はファイルを削除する。
要は、テストコードにて、実環境に永続化されるような処理がある場合は、その初期化をきちんとやりましょう
ということになります。
これは、E2Eテストのように検証すべき項目が多くなり、関連するデータストアや外部システムが増えるほど、
考慮するのが難しくなってきます。テストコード作成における腕の見せ所になるかと思います。
まとめ
テストコードに関して、6つの観点から記事を書かせていただきました。
テストコードの保守・運用には、プロダクションコードとは異なる難しさがあると感じています。健全な開発ライフサイクルを回していくためにも、プロダクションコードと同様に、技術的負債にならず、長生きするテストコードを書いていきたいなぁと、日々思っている次第です。