自動テストの重要性が広く認知されるようになった一方、自動テストの活用に課題を抱える組織も依然として多く見受けられます。

本記事では『Developer eXperience Day 2024』(主催:日本CTO協会)における和田卓人氏によるセッション「望ましい自動テストとは:どのようなテストが開発生産性と開発者体験を共に高めるのか」の内容をお届けします。


和田卓人氏
執筆活動や講演、ハンズオンイベントなどを通じて自動テストやテスト駆動開発を広めようと努力している。 『プログラマが知るべき97のこと』(オライリージャパン、2010)監修。『SQLアンチパターン』(オライリージャパン、2013)監訳。『テスト駆動開発』(オーム社、2017)翻訳。『事業をエンジニアリングする技術者たち』(ラムダノート、2022)編者。

なぜ自動化テストを書くのか

和田 卓人です。インターネット上ではt-wadaと呼ばれています。まずは序論として、自動テストをなぜ書くのかについてお話しします。

テスト自動化の目的としてコスト削減を掲げることが多いですが、これはアンチパターンです。結果としてコストが削減される面はありますが、短期的には自動テストの学習コスト、中期的には保守コストがのしかかってきます。

そのため、「あまりコスト削減効果がない」と手動テストに戻ってしまう現場を多く見てきました。

ソフトウェア開発では社会や世の中の変化についていくことが大切で、自動テストは変化を可能にする重要な技術プラクティスの一つです。ソフトウェアは変化してナンボであり、”ソフト”であるべきです。しかし、保守性が低下するとソフトウェアを変更するのが難しい、”ハード”な状況になってしまいます。これは悲しいことです。

コスト削減のためではなく、素早くちゅうちょなく変化し続ける力を得るために、自動テストを書いていく。これが今日の結論のひとつです。そして、望ましい自動テストとは以下のようなもの、というのがもうひとつの結論です。

私が考える自動テストの目的は、信頼性の高い実行結果に短い時間で到達する状態を保つことで、開発者に根拠ある自信を与え、ソフトウェアの成長を持続可能にすることです。望ましい自動テストの要素を一つずつ紐解いていきましょう。

信頼性の高い自動テスト

自動テストの信頼性が高いとは「嘘がないこと」を意味します。嘘とは、具体的には「偽陽性」と「偽陰性」の2つです。これらがなければ、テストが信頼できます。

信頼できるテストは、成功していればリリースやデプロイができ、失敗していればコードに直すべきところがある、というように、自分たちの行動を2択に整理できます。これは生産性が高く、開発者体験が良い状態です。

信頼性を維持するためには継続的な努力が求められますが、それだけの価値があります。
では、偽陽性や偽陰性にはどのようなパターンがあるでしょうか。
偽陽性のパターン
大きく2つのパターンがあります。

  • 脆いテスト(brittle test, fragile test)
  • 信頼不能テスト(flaky test)

信頼不能テスト(flaky test)とは、プロダクトコードやテストコードに一切手を触れなくても結果が成功・失敗に揺れる不安定なテストです。flaky testが多発すると、テストの失敗に対して皆鈍感になります。

『Googleのソフトウェアエンジニアリング』によると、テストスイートの1%がflaky testだとエンジニアはテストの結果を信じなくなります。そのため、flaky testは可能な限り減らす必要があります。

もう一つは脆いテスト(brittle test, fragile test)、コードに手を触れるとすぐに失敗するテストです。

たとえば、モックだらけのテストやプライベートメソッドに触れているテストなど、外から見た振る舞いが変わっていないにもかかわらず、コードを少し変更するだけで多くが失敗し始めることがあります。テストと実装の構造的結合度が高すぎたり、不必要に詳細までテストしていたりすると、脆いテストになります。

偽陰性のパターン

大きく3つのパターンがあります。

  • 空振り
  • カバレッジ不足、テスト不足
  • 自作自演

最もベタなのが空振りです。テストを実行しているつもりでスキップしていたというケースが現場でよく見られます。

次にカバレッジ不足やテスト不足です。カバレッジ不足というのは、書かれるべきテストコードが書かれていない、テストすべき行をテストが通っていない状態です。これはコードカバレッジツールを使えば可視化でき、対応可能になります。

一方、手強いのはテスト不足のほうです。これは書かれるべきコードが書かれていない、つまり人間が仕様レベルで把握できていない状態です。

最後に、自作自演があります。よくある例として、プロダクトコードとテストコードが同じロジックで計算しているために、バグがあってもテストが成功してしまうケースです。

このように、実質テストになっていない自作自演のテストが、現場では非常によく見られます。

自動テストの実行結果

私は、自動テストの実行結果は単なる結果ではなく「情報」であるべきだと考えています。情報とは、それを見た人間に意思決定と行動を促すものです。

先に申し上げたように、テストの結果が信頼できるものであれば、人間の行動は2択になります。緑=成功であれば前に進め、赤=失敗であればコードを直せ、というわけです。

自動テストの失敗には2種類あります。

  • Execution Error:実行中にプロダクトコードのどこかから発生する実行時エラー
  • Assertion Failure:テストコード中に書いた表明(アサーション)の失敗

それぞれ取るべき行動が若干異なります。

Execution Errorの場合は、スタックトレースなどからどこで失敗したかを調べ、何が起こったのかを推測します。

対してAssertion Failureの場合はエラーなく結果が戻ってきているけれども結果がおかしいため、失敗の原因を探る必要があります。こちらのほうが若干難易度が高いです。アサーションの書き方は失敗時に輝きます。失敗時に助けになるアサーションを書かなければなりません。

単なる論理式で「trueが期待されるところでfalseでした」だけでは、情報が不足しています。次に取るべき行動がわかりづらいです。テストに成功しているときはこの脆さには気づきません。テストが失敗したときに初めて、このテストは頼りにならないな、と気づきます。

たとえば、具体的な値を結果に表示しましょう。40でなく39だったのか、0だったのか、NULLだったのか、など細かいところで調査の初動が変わってきます。こうした細かいところが、生産性の差として現れてきます。

短い時間で到達する

皆さんに質問です。それぞれYes/Noで答えてみてください。

  • データベースにアクセスするのはユニットテスト?
  • ネットワークにアクセスするのはユニットテスト?
  • ファイルにアクセスするのはユニットテスト?
  • 現在時刻にアクセスするのはユニットテスト?
  • 依存先のモジュールに本物を使うのはユニットテスト?

正解はありません。ここで言いたいのは、人によって答えがバラバラであるということです。

ユニットテストのユニットとは何か、がブレている状態で議論をしても前に進みません。もっと明快な基準で、かつCIや開発のリードタイムに直接影響するものはないのでしょうか。

自動テストとCIにフィットする明確なテスト分類基準として「テストサイズ」という考え方があります。

Unit・Integration・E2Eとは別軸で、Small・Medium・Largeという分類です。

Smallテストはテストの実行がひとつのプロセスに収まっているものです。
Mediumテストはひとつのマシンに収まっているもの、そしてひとつのマシンに収まっていないものをLargeテストと呼びます。非常に明快な基準です。

テストの安定性や速度に最も影響を与えるのはネットワークアクセスです。ネットワークアクセスを狭めていけば、テストが速く、安定したものになります。

これらのテストサイズと、従来用いられているUnit・Integration・E2Eとは3×3の掛け算で表せます。

マスの中にあるのは私個人の意見、オススメ度です。そして、真ん中のラインでコスパ、投資対効果が分かれます。

右上に行くほどコスパが悪い、書いても助けにならないものです。左下に行くほど書いてお得になります。E2EでSmallは原理上不可能に近いですが、小さいシステムならできるかもしれません。

オススメのコスパ領域はIntegration×Smallです。

状態を保つ

では、信頼性の高い実行結果に短い時間で到達するためにどのような状態を保てばいいか、望ましいテストの比率とはどういったものかについてお話しします。

有名な”テストピラミッド”という概念があります。これを使うことで、自分たちが書いてきたテストを健康的な状態に保とうというわけです。

よく説明されるのはこの図です。

このモデルは20年以上前のもので、テストピラミッドはもう古い、といった議論もたくさん出ています。

アンチパターンとしてのアイスクリームコーンというモデルや、ピラミッドではなくトロフィー型やハニカム型のモデルも登場しています。そうなると、我々一般開発者が困ってしまうわけですね。ピラミッドが良いと思っていたら、もう古いのでトロフィーが良い、ハニカムが良いなどと言われてしまう。

しかし問題は語彙なんです。どのモデルも、さきほど説明したブレブレの軸で話をしている。もっとブレのない定義はないでしょうか? もう話をしてきました。テストサイズですね。

テストサイズでピラミッドを構築したほうが基準が安定し、議論のブレがなくなります。

また、さきほど出てきたトロフィー型やハニカム型のモデルも、テストサイズの観点で並べ直してみると、ピラミッド型に近づいていきます。テストサイズでピラミッドを作りましょう。

では、どうやってピラミッドをつくっていこうか、という話に移ります。

多くの現場では、最初はアイスクリームコーンから始まります。これは悪いことではありません。最初からピラミッドをつくるのは難しいです。

問題は、アイスクリームコーンが長続きしてしまうことです。この形が続くと、不安定なテストが多く開発の助けにならずに辛くなっていきます。

アイスクリームコーンを、どのようにピラミッドにしていくのか。まずはLargeテストを減らしてMediumテストに移していくことから考えましょう。

Largeテストは、一つのマシンに収まらないテストでした。これを、テストダブルを用いて一つのマシンに収まるテストにしていきます。

たとえばDynamoDBはクラウド上にあるので、使うと絶対にLargeテストになります。そこでテストダブル(Fake)としてのDynamoDB localを使うことで、信頼性を大きく下げずにテストサイズを下げることが可能です。

次はMediumテストからSmallテストへ下げていくことを考えます。

1つのプロセスの中にどのようにテストを収めるか。これは設計の話になるんですね。テストしにくいところを薄く切り離してテストしたいところを大きく露出させることで、テストが1プロセスに収まります。これは良い設計、低結合高凝集になるような設計をすると、テストサイズが下げやすくなる、ということでもあります。

テストのサイズダウンは、このようにやっていきましょう。表の右下から始めて、まずはMedium×Integrationに下げる。そこからSmall×Unit、Medium×Unit、Small×Integrationに下げていくのがオススメです。

講演のまとめ

この講演でお伝えしてきたのは、ピラミッドはテストサイズで構成しましょう、ということです。

そして、テストダブルを使うことによってLargeからMedium、MediumからSmallに下げ、アイスクリームコーンからピラミッドに近づけられます。
このような流れで、信頼性の高い実行結果に短い時間で到達できる状態を長期的に維持する、というテスト戦略を立てていきましょう。

取材後記:理想の自動テスト群を目指して

本講演では、自動テストの目的から始まり、望ましい自動テストにするための考え方としてのテストサイズ、そしてアイスクリームコーン型をどのようにピラミッド型に近づけていくかについてお話しいただきました。

テストピラミッドの考え方は開発現場で広く普及してきている実感があります。一方で、理想の実現が難しく、アイスクリームコーン状態で停滞してしまっているという現場も多く見られます。そうした中で、今回の講演は多くの開発チームにとって、理想の自動テストを実現する助けになると感じました。

(撮影/文:伊藤由貴

presented by paiza

Share

Tech Team Journalをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む