見出し画像

名著から学ぶ、単体テストの勘所|QAエンジニアのブログ


はじめに

はじめまして!SHIFT QAエンジニアの森井と申します。
この記事では、巷で話題の『単体テストの考え方/使い方』を参考に、「単体テストをプロジェクトの資産にしていくための勘所」を考えていきます。
「単体テスト」は、使われる場面や人で意味合いが変わりますが、ここでは「単一の振る舞いを検証する、テストコードによるテスト」を想定しています。この点は後ほど詳述します。
テストコードの作成に携わる方、今後自動テストを取り入れてみたい方に向け、「何に注意すれば、質の高いテストコードを作成できるか」のヒントをお届け出来ればと思います。

(どんな本?)

今回取り上げる『単体テストの考え方/使い方』を簡単に紹介します。
著者は、Microsoft社のMVPソフトウェア・エンジニア、Valdimir Khorikov(ウラジミール・コリコフ)。2022年12月末に、マイナビ出版から発刊されています。
「プロジェクトに活きる単体テスト」の理念と、実際に作成していくコツを検討していく書籍です。

用語整理

この記事では、V字モデル開発を念頭に、最下層のテストレベルである「単体テスト(コンポーネントテスト)」の品質向上を考えていきます。
V字モデルについては、以下の記事で詳しく書かれています。宜しければ合わせてご参照ください。
| V字モデルとは|ウォーターフォール型開発における品質面でのメリット

『単体テストの考え方/使い方』では、単体テストを次にあげる3つの性質を備えたテストと定義します。

・1単位の振る舞い(a unit of behavior)を検証すること
・実行時間が短いこと
・他のテスト・ケースから隔離された状態で実行されること

実行という言葉選びが特徴的です。これは単体テストを、自動テストされるもの(システムがテスト実行するもの)という前提で定義しているためです。

本書における単体テストは、「製品コード(プロダクション・コード)と共に実装され、CI/CDツールCronなどで、定期的に実行されるテスト」のイメージです。
より具体的には、SeleniumやxUnit、Postmanなどで実装されるテストです。機械的にプログラムを呼び出し、レスポンスや値が期待するものであることを確認するテストプログラムが、今回取り上げる「単体テスト」の内容です。この記事では、特にバックエンドテスト(サーバー側)における単体テストを念頭に話を進めるので、フロントエンド(クライアント側)のテストをメインに行われる方は、ややイメージしにくい部分があるかもしれません。ご承知おき下さい。

単体テスト/自動テスト/テストコードは、古くからある考えです。その反面で課題も多く、十分に活用が進んでいない印象もあります。かくいう私も、今のプロジェクトに携わるまでは、本格的なテストコードの実装は行ってきませんでした。
自動テスト初心者の私が、本書を手に取り発見した「単体テストの勘所」を、以下お伝えしていきます!

単体テストの勘所(考え方編)

なぜ単体テストを作るのか?

単体テスト(テストコード)の作成には労力がかかります。そのため、単体テスト作成に誤った方針や目標を立ててしまうと、単体テストはプロジェクトの負担になってしまいます。
そもそも、なぜ単体テストを行うのでしょう?本書では次のように単体テストの目標を定義します。

単体テストを行うことの目標は、ソフトウェア開発に関するプロジェクトの成長を持続可能なものにする、ということである。質の良いテスト・ケースで構成されたテストスイート(*)を用意することでプロジェクトが停滞してしまうことを回避し、適切な開発スピードを維持できるようになる。

(*テストスイート:特定のテストサイクルで実行されるテストケースやテスト手順のセット)

簡単に実施できる単体テストがあれば、製品コードに一定の秩序を保ち続けることが可能になります。それにより、機能の変更や追加に対し、リグレッション(退行)が起きない自信が持てるようになり、適切なスピードで開発を進めていけるようになります。
つまり、単体テストは製品開発のセーフティネットだという考え方です。

良い単体テストの姿とは?

コードは、書けば書くほどバグを生み出す可能性を上げます。本書では、「コードは資産でなく負債として見るべき」と指摘しており、テストコードにおいても最小量で実装すべきとしています。
よくある単体テストの方針に、カバレッジ(網羅率)による目標設定があります。例えば、「全コードのN%までは単体テストを書きましょう」であったり、「全ての分岐を通るように単体テストを書きましょう」といった目標です。本書ではこうした目標設定に否定的です。前述の通りでコード量が増えてしまう上、機械的な目標設定は開発の妨げになりやすく、不必要に保守性能を低めてしまうという理由です。

単体テストの質を向上には、カバレッジだけでは不十分で、テストコード/テストケースの良し悪しを区別できる経験知が必要になります。対象のテストが優れているかを自動的に判断する手段はなく、究極的には個人の判断に委ねられるというのが著者の見解です。
良い単体テストは何かを判断する具体的な基準は、本書を通し検討が進められますが、ここでは「優れたテストスイートが持つ共通点」として挙げられていた、以下の特徴をピックアップして紹介します。

優れたテストスイートには次の特徴がある

1.テストすることが開発サイクルの中に組み込まれている
2.コードベースの特に重要な部分のみがテスト対象になっている
3.最小限の保守コストで最大限の価値を生み出すようになっている

まずはこれらの”特徴”を達成していけば、大きく失敗することはないのではと思いました。

「特徴1」の達成は、現場の環境に左右されますが、CI/CDツールに組み込めれば、十分なタイミングでテスト実行が出来そうです。「特徴2」の達成は、ハッピーパスなどの考えからテストケースを選定する戦略が有効そうです。「特徴3」の達成は、単体テストの支援クラス(テスト・フィクスチャ)を整備する戦略が考えられます。個々の単体テストで共通する部分を、テスト・フィクスチャにまとめることができれば、全体のコード量を減らすことができ、「最小限の保守コスト」を高い水準で達成できるようになります。とはいえ、「単体テストは最小限のコード量で実装する」という考え方が無ければ、なかなかコード量は減っていかないので、まずはその確認が大切です。

まとめ

考え方編は以上です。自分は特に以下を念頭に置くことが大切ではないかと思いました。

  1. 単体テストはセーフティネット。製品開発のスピードを適切に保っていくために作成する。

  2. テストコードは資産でなく負債。最小の実装で最大の成果を出すことを意識する。

  3. カバレッジに囚われて単体テストを作成しても、効果が高められるとは限らない。有効なテスト対象・テスト手法を学習する必要がある。

単体テストの勘所(使い方編)

単体テストの3つの手法

次は単体テストの実装において使えそうなテクニックについてシェアしていきます。

とはいえ、数多あるソフトウェア開発において、汎用的に活用できるテクニックを考えることは大変です。一口に単体テストと言っても、代表的な手法には次の3つがあると本書では述べています。

・出力値ベース・テスト:テスト対象システムに入力値を与え、そこで生成された結果を検証する手法

・状態ベース・テスト:テスト対象システムと協力オブジェクト
(データベースやメールサーバーなど)の状態を検証する手法

・コミュニケーション・ベース・テスト:モックを使ってテスト対象システムと協力オブジェクトのコミュニケーションを検証する手法

私は主に、APIのレスポンスを確認する出力値ベース・テストを使っています。『単体テストの考え方/使い方』の著者は、出力値ベース・テストを保守コストを最も低く扱えるテスト手法と評価しており、単体テストはこの手法を基本とすることを推奨しています。
自分の力量を考えて、ここでは出力値ベース・テストに限定して、その使い方を紹介していこうと思います。

出力値ベース・テストはいつ使えるか

出力値ベース・テストは、対象システムのレスポンスのみを確認するテストです。裏を返すと、システムに以下のような「隠れた入出力」がある場合、この手法は使えません。

・副作用:メソッド・シグネチャに表現されていない(コード記法上に明示されない)出力。例として、ディスク上のファイル内容を更新すること。それは「隠れた出力」となる。

・例外:メソッドが例外をスローして、それが何層にも重なった呼び出しのどこかでキャッチされる作りになっている状態。それは「隠れた出力」となる。

・内部もしくは外部の状態への参照:メソッド内部でDateTime.Nowのように実行タイミングの日時を取得したり、データベースからデータを取得したりしている状態。それは「隠れた入力」となる。

上記のような「隠れた入出力」がテスト対象にある場合は、出力値ベース・テスト以外のテスト手法も取り入れて、品質担保を行う必要があるかもしれません。
「隠れた入出力」がない関数は、数学的関数ないし純粋関数と呼ばれます。数学的関数のみでアプリケーションを構築することは不可能ですが、「隠れた入出力」のある関数とない関数を分離させ、役割分担を明確にした構造を作る手法は研究されています。具体的には、「関数型アーキテクチャ」や「ヘキサゴナル・アーキテクチャ」とされる手法です。本書の終わりには、「隠れた入出力」が含まれるコードを、保守性の高い数学的関数に置き換える(リファクタリングしていく)方法を紹介しています。具体例を示すと、その部分で話が長くなるので割愛します。興味がある方は本書を読んでご確認お願い致します。

出力値ベース・テストの作り方

さて、出力値ベース・テストを実施できると確認できた場合、どのようにその質を高めていけるでしょうか。
単体テストは繰り返し利用されるので、保守性が高いこと(メンテナンスがしやすいこと)は重要です。著者は、単体テストは基本的に「AAAパターン」という、次に示す3つの構造からなるとします。

・Arrange(準備)フェーズ:テストケースの事前条件を満たすように、テスト対象システムとその依存状態を設定するフェーズ

・Act(実行)フェーズ:テスト対象システムのメソッドを呼び出すことで、テスト対象の振る舞いを実行させるフェーズ

・Assert(確認)フェーズ:実行結果が想定した結果であることを確認するフェーズ

「単体テストはAAAパターンからなる」という認識を持つだけで、単体テストが作成しやすくなるように思います。各フェーズの始まりにコメントを入れるとか、空白行を入れるとかのコーディングルールを設けるようにすれば、一層、チーム全体で保守性を高めていけるかもしれません。
また反対に、「AAAパターン」に則らない単体テストは、見通しが悪くなるため、アンチパターンと考えられます。具体的には、1つのケースで複数のArrangeフェーズがあったり、条件分岐がある場合などが考えられます。その場合は、テストを分割したり、テスト内容をメソッド化して、パラメータを渡す形に変更する方法で改善が行えるかもしれません。

まとめ

使い方編は以上です。自分は特に以下を実務に取り入れられると思いました。

  1. テスト対象に「隠れた入出力」がないか確認する。それがある場合は、テスト手法をよく考える。

  2. 単体テストは基本的に「AAAパターン」からなる。各フェーズで何をするか、想像してからテストコード作成に取り掛かるのがよさそう。

  3. プログラムの構造によっては、出力値ベース・テストが使いにくい場合がある。その場合は、プログラムがどのような構造か/現状の構造に問題がないか、理解を深めていくことも大切。

おわりに

以上、簡単ですが、『単体テストの考え方/使い方』を読んで分かった「単体テストの勘所」です。

単体テストの勘所(再掲)

考え方編

  1. 単体テストはセーフティネット。製品開発のスピードを適切に保っていくために作成する。

  2. テストコードは資産でなく負債。最小の実装で最大の成果を出すことを意識する。

  3. カバレッジに囚われて単体テストを作成しても、効果が高められるとは限らない。有効なテスト対象・テスト手法を学習する必要がある。

使い方編

  1. テスト対象に「隠れた入出力」がないか確認する。それがある場合は、テスト手法をよく考える。

  2. 単体テストは基本的に「AAAパターン」からなる。各フェーズで何をするか、想像してからテストコード作成に取り掛かるのがよさそう。

  3. プログラムの構造によっては、出力値ベース・テストが使いにくい場合がある。その場合は、プログラムがどのような構造か/現状の構造に問題がないか、理解を深めていくことも大切。

本書を読んでみて、抽象的な「単体テスト」に対し、具体的なイメージを持つことができました。上記のようなポイントをチームで共有し言語化していくことで、品質担保の方向性をブラさずに高めていけるのではないかと思います。本書には、まだまだ紹介しきれていない単体テスト作成のコツがあるので、興味がある方は是非ご一読ください。
それでも迷うところがあれば、お気軽にSHIFTにお声がけを!

最後までお読みいただきありがとうございました!
少しでも参考になる点があれば幸いですmm


執筆者プロフィール: 森井直彬
新卒でITベンダーで開発に従事。より顧客に沿ったサービス提供を目標に2021年末にSHIFTに入社。
新しい物好き、活字好き。最近の興味はブロックチェーンと、在宅でも簡単に作れる料理。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

PHOTO:UnsplashMikołaj