テストはどのくらい書けばいい?「効率的なテスト」を考える3つの視点
テストが不十分なコードを扱うのがどれほど辛いか、経験したことがある人も多いでしょう。バグを1つ直すたびに、新たなバグが5つも生まれる。そして、たまたま動いているように見えても、それが設計どおりの挙動なのか、単なる偶然なのか、確信が持てません。
一方で、小さな機能を1つリリースするために、200個ものテストを書いた経験はありませんか。カバレッジ100%を達成するために、すでに問題なく動いているコードを何度も作り直す。せっかくテストを厚くしたコードほど、かえって読みにくくなっている気がする。そして最悪なのは、アプリ開発そのものに疲れ果ててしまうことです。
どこかにちょうどいい落とし所があるはずです。では、テストはどのくらい書くのが適切なのでしょうか?
「テストコードはアプリのコードの2倍」「カバレッジは95%」といった、きりのいい目安があれば便利ですが、そもそも「95%のカバレッジ」という数字自体が曖昧です。
カバレッジはテストが充実していることの指標にはなりますが、テストの質を保証するものではありません。私自身、カバレッジ100%のアプリよりも、85%のアプリの方がバグが少なかったという経験があります。
つまり、適切なテスト量は数字では測れないのです。重要なのはもっと曖昧で定義しづらいもの――いかに「効率的に」テストするかということです。
効率的なテストとは
効率的なテストとは、最小限の手間で最大限の効果を得ることです。理想ですよね。
ただし、テストを効率化するには考えるべきことがたくさんあります。特に意識したいのが「サイズ」「分離」「フォーカス」の3つの観点です。
サイズ:統合テストとユニットテストのバランス
統合テスト(インテグレーションテスト)は強力です。実際のユーザーがアプリを通る経路をそのまま再現し、本番と同じ使い方で、すべてのコードが連携して動く様子を検証できます。
しかし統合テストは遅く、長く複雑になりがちです。システムのごく一部を徹底的に検証したい場合には、オーバーヘッドが大きすぎます。
ユニットテストは小さく、高速に実行できます。記述中に頭に入れておくべき範囲が狭いため、考えやすさの面でも優れています。
ただし、ユニットテストは「偽物」になり得る点にも注意が必要です。ユニットテストで動いていても、実環境で正しく動くとは限りません(モックを多用している場合は特にそうです)。
では、この2つをどうバランスさせればいいのでしょうか?
ユニットテストは速く簡単に書けるため、数を増やしてもコストがほとんどかかりません。エッジケースや複雑なロジックの検証は、ユニットテストに任せるのが最適です。
システムの各パーツを十分にテストできたら、次は隙間を埋めます。パーツ同士の連携や、ユーザーがアプリ内で辿りうる一連の流れを検証するわけです。しかし、エッジケースやロジックの大半はすでにユニットテストでカバーされているため、必要な統合テストはごく少数で済みます。
この考え方は「テストピラミッド」と呼ばれています。多数のユニットテストを土台に、その上に少数の統合テストを載せる構造です。詳しく知りたい方は、拙著『Practicing Rails』の第3章をご覧ください。
分離:依存関係を断ち切る
とはいえ、システムが複雑だと、あらゆる状況をカバーするのに膨大な数のテストが必要になることがあります。それはアプリの設計を見直すサインかもしれません。システムの一部同士が、強く結合しすぎているのです。
例えば、いくつかの状態を取りうるオブジェクトがあるとしましょう:
case user.type
when :admin
message = admin_message
when :user
message = user_message
when :author
message = author_message
else
message = anonymous_message
end
if user.preferred_notification_method = :email
send_email(message)
elsif user.preferred_notification_method = :text
send_text_message(message)
else
queue_notification(message)
end
ここで全経路をテストしようとすると、12通りのケースが必要になります:
- ユーザーが管理者(admin)、
preferred_notification_methodが email - ユーザーが管理者(admin)、
preferred_notification_methodが text - ユーザーが管理者(admin)、
preferred_notification_methodがどちらでもない - ユーザーが一般ユーザー(user)、
preferred_notification_methodが email - ユーザーが一般ユーザー(user)、
preferred_notification_methodが text - ユーザーが一般ユーザー(user)、
preferred_notification_methodがどちらでもない - ユーザーが投稿者(author)、
preferred_notification_methodが email - ユーザーが投稿者(author)、
preferred_notification_methodが text - ユーザーが投稿者(author)、
preferred_notification_methodがどちらでもない - ユーザーが匿名、
preferred_notification_methodが email - ユーザーが匿名、
preferred_notification_methodが text - ユーザーが匿名、
preferred_notification_methodがどちらでもない
これほど多くのケースが必要になるのは、「通知方法に応じてメッセージを送る処理」と「ユーザーの種類に応じてメッセージを生成する処理」が密接に結びついているからです。工夫すれば減らせるかもしれませんが、判断は容易ではなく、バグを招きやすい状態です。
では、2つに分割したらどうなるでしょうか?
message = get_message_based_on_user_type(user.type)
send_notification(message, user.preferred_notification_method)
これで、それぞれの部分を独立してテストできるようになります。
前半については、ユーザーの種類ごとに正しいメッセージが返されることをテストします。
後半については、指定されたメッセージが preferred_notification_method の値に応じて正しく送信されることをテストします。
最後に、親メソッドが前半の結果を後半に正しく渡していることを確認するテストを1つ追加します。こうして、テストすべき状態は8つに減りました:
- ユーザーが管理者(admin)である
- ユーザーが一般ユーザー(user)である
- ユーザーが投稿者(author)である
- ユーザーが匿名である
preferred_notification_methodが email であるpreferred_notification_methodが text であるpreferred_notification_methodがどちらでもない- 親メソッドの受け渡しを確認するテスト1件
コードを分割して独立にテストできるようにすることで、4つのテストを削減できました。2つ目の例の方が、テストを減らせることは一目瞭然です。状態がさらに増えるほど、コードを分割するメリットは大きくなると想像できるでしょう。
分離と可読性の最適なバランスを見つけるには、時間と練習が必要です。しかし、依存関係を適切な場所で断ち切ることができれば、必要なテストの数は大幅に減らせます。
フォーカス:どこに注力するか決める
アプリはしっかりテストされているべきです。しかし、だからといって、アプリのすべての部分に同じだけのテスト工数をかけるべきだという意味ではありません。
カバレッジ100%を目指しても、結局すべてはテストできません。例えば、ビュー内のテキストの一行一行や、「更新確認を10秒ではなく5秒ごとに行っているか」などは、おそらくテストしないでしょう。
そこで重要になるのがフォーカスです。より少なく、より有用なテストを書くこと。そして、限られた時間をどこに投じるのが最善か、意識的に決めることです。
フォーカスもまた、習得が難しいスキルです。私が最も重要なテストに集中するために自問している質問をいくつか紹介します:
- この部分はアプリの他の部分とどの程度結びついているか?壊れた場合、他にいくつの機能が巻き込まれるか?
- この部分は自然に変更される可能性が高いか?テストが失敗したとき、それはバグのせいか、それともUIの文言が更新されただけか?
- この部分が壊れたときの影響は?クレジットカードへの二重請求が起きるのか、それとも表示テキストが欠ける程度か?
- この部分はどのくらい使われているか?アプリの中核を担う機能か、フッターの奥に埋もれた「Aboutページ」程度か?
重要な部分だけをテストすればいい、という意味ではありません。しかし、テストの時間を賢く使えば、アプリ全体の品質感は確実に上がります。
まとめ
ユーザーが取りうるすべての経路を網羅しようとしたら、永遠にリリースできません。TDDは有効ですが、テストに関する問題をすべて解決してくれるわけではないのです。
もちろん、だからといってテストしなくていいという意味ではありません。
テストピラミッドを活用してテストを小さく保ち、依存関係を断ち切って m × n のテストケースを m + n に減らし、優先順位をつけてアプリの最重要部分に時間をかける。そうすることで、過剰でも不足でもない、ちょうどいいテストに近づけます。
あなたはどのくらいテストしていますか?アプリを開発する際に、これらの考え方を意識していますか?そして、アプリのどの部分に注力すべきかをどう判断していますか?ぜひコメントで教えてください!
-
MacBook Proの価値はいくら?下取りと個人売買で高く売る方法を徹底解説
MacBook Proの価値は、主に「年式」「状態」「売却方法」の3つの要素によって決まります。一般的に、個人売買の方が小売店での下取りやAppleのTrade Inプログラムよりも高く売れる傾向があります。 目安としては、比較的新しく状態が良好なMacBook Proであれば、購入価格の40〜65%程度で売却できることが多いでしょう。 筆者はApple製品に精通した専門家で、2019年製16インチMacBook Proの所有者でもあります。これまで数多くのMacを売買してきた経験をもとに、あなたのMacBook Proの価値を見積もるためのガイドをまとめました。 それでは、あなたのMa
-
Discordでユーザーを通報する方法【PC・スマホ完全ガイド】
Discordは、世界中のゲーマーの間で最も人気のあるプラットフォームの一つに成長しました。しかし、ユーザー数が膨大である分、不正な行為を行うユーザーや、Discordの利用規約に違反するユーザーに出会ってしまう可能性もあります。そのためにDiscordには通報(レポート)機能が用意されており、攻撃的・不適切なコンテンツを投稿するユーザーを報告できるようになっています。ソーシャルメディア上での通報は、プラットフォームの健全性を保つために今や一般的な手段となっています。 通報自体はシンプルな操作ですが、ITに不慣れな方にとっては少し難しく感じられることもあるでしょう。この記事では、デスクトップP