自動テストの戦略と戦術
聞かれることが多いので文章としてまとめておく。参照しながら説明するための資料となる。そのため、あるていど自動テストに慣れた方でないとスムーズに読めない可能性がある。わかりづらい部分があればぜひ編集リクエストを送ってほしい。
私の立ち位置を明らかにしておくと、「好みの問題」になるまでは原理主義的に自動テストを書いていくべきだと思っている。
が、組織やチームによってそのラインも異なるし、もちろん異なるスタンスの方もいるので臨機応変に使えるものを使えば良い。
内容は和田拓人さんの発信からの我流なので、少しでも気になる情報があれば、一次情報にあたる意味でも彼からの情報を追うと良い。もちろん、矛盾や不備などは我流部分なので指摘は私へ。
なお、戦術という言葉は「コードベースに携わるチームが目標を達成するためのレイヤー」という意味で、戦略という言葉は「チームも含めた集団全体の目標を達成するためのレイヤー」という意味で使っている。裏返すと、ここで戦略として書いたことはエンジニアでなくても理解可能、かつ理解しておいたほうが良い内容となっている。
3 行まとめ
戦略
- 目的をはっきりさせよう
- 効果的に開発したいなら書こう
- ただしこだわりすぎないように
戦術
- 無理がないように徐々に習得していこう
- 細かく分割して書いていこう
- テスティングトロフィーを参考にしよう
戦略
常に目的をはっきりさせる
あるコードベースに自動テストが存在することは、人間にとって次の旨みがある。
- 品質保証
- 動く仕様書
- 設計のためのツール
組織目標を達成するために、これらのメリットをどのように使うかを意識すると良い。楽をしようとするなら総取りが良いが、それぞれで学習が必要なため徐々にやっていこう。
品質保証
自動テストは品質保証の一部、ソフトウェアテストのさらに一部として定義されている。そのため、やるかやらないかではなく「どこまでやるか」を決めることになる。
存在する自動テストをすべてパスしていればプロダクトの目標を達成している、これが理想の状態だ。ただし現代で理想を実現するのは現実的に不可能なので、組織目標とコストを勘案しながら決めることになる。
理想の実現が不可能な理由
最も大きいのは我々が自身のことを十分理解していない、ということだ。現状、プロダクトごとにユーザーリサーチが必要になる。これは求める品質が不明確であることの証左だ。人間がどのように変化するかも含めての理解が人類になければ、自動化は不可能だ。
もちろんユーザーのことがわからなければ目標を定められない。目標がないものの自動テストは書けない。
理想を実現するために必要なコストが莫大となることも現実的な課題だ。目標を達成しているかの検証を自動化するのはコストパフォーマンスが低い。
他の目標との兼ね合いもあるが、おおむね次に挙げる段階があるだろう。
- 書けるところだけ書く
- コアドメインのテストを書く
- UI のテストを書く
- UX を可視化する
書けるところだけ書く
コードベースの大きさによらず、テストハーネスの導入など土台の整備と、その動作確認のための簡単な自動テストを最優先で用意しよう。
たいていのテストハーネスにはチュートリアルが整備されているので、まったく書いたことがない方はそこから始めるのがいいだろう。
また、「テストがないコードベースにテストを追加する」のは実はかなり難度が高い。次が主な理由だ。
- そのようなコードベースでは得てしてテストを書きやすい部分が少ないこと
- 書きやすいところを見分けるための知識と経験がメンバーに蓄積されていない可能性が高いこと
- テストを書くためにコードを変更したい場合、その変更がプロダクトを壊していないことを確かめるためのコストが高いこと
- 手動テストでカバーする場合、何度も変更とテストを繰り返すため時間コストが高い
- 自動テストを用いる場合、必要な知識と経験が増えてしまうこと
どのように自動テストを導入するかについてはレガシーコード改善ガイドが詳しい。
オブジェクト指向全盛期に書かれた本だが、エッセンスはその他のパラダイムでも使えるはずだ。
ここまでできていれば、徐々に自動テストは増えていく。不安に感じている部分のテストを厚く書いたり、トラブルの再発を防止するためにテストケースを追加したりなどだ。テストを書きたいと思った際にすぐ書ける状況を維持しておくのが最も重要だ。こういった意味で基礎を固めながら書けるところだけ書くのは現実的な戦略だ。
コアドメインを優先して書く
ここでいうコアドメインとは DDD で定義されているものだ。経営的には投資対象であり金のなる木であり企業の存在価値となりうるものだ。
意図通りにコアドメインを表現できていなければ、プロダクトとして成立しないはずだ。逆に言えば、これこそが優先的に自動テストの対象とすべき理由となる。自動テストをツールとして用いることでコアドメインの機微を明文化する。
コストパフォーマンスが高いことも理由として大切だろう。後述するように、自動テストを動く仕様書として考えた場合、コアドメインの仕様がわかる資料の作成と品質保証活動を同時に実施可能となる。
また、クリーンアーキテクチャーや DDD など複数の層構造を持つ設計論を採用する場合、最も自動テストが書きやすい。というのも、それらの設計論においてはまったく依存を持たない層となるため、必然、自動テストを書く難度が低くなる。次の図における最も内側の円の部分がここにあたる。
コアドメインはテストカバレッジ 100% とすることを目指すと良い。ここで対象としているカバレッジ基準はできれば分岐カバレッジや条件カバレッジとしたい。これが難しい場合、それはコアドメインではない可能性がある。
UI のテストを書く
自分でハードウェアから作らない限り、 UI は既存の仕組みの上に作っていくことになる。必然、フレームワークやライブラリーなどに依存する必要が出てくる。
これは非常に自動テストを書きづらい。フレームワークやライブラリーのテストをしたいわけではないが、自動テストを書こうとするとそれらも含んだものになりがちだ。
こういった場合はハンブルオブジェクトという概念を用いて、描画ロジックを対象とした自動テストを書くと良い。実際の例として React におけるハンブルオブジェクトの活用は次で紹介している。
また、見え方が意図通りかという課題はそのまま扱うことが難しい。これは UI コンポーネントとしてより小さな粒度で見た目を担保することで画面全体の品質を確保する。具体的にはデザインシステムを構築し、ビジュアルリグレッションテスティングなどを実装すると良い。
UX を可視化する
今までの話題は「正しく作る」ための方法、この話題は「正しいものを作る」ための方法論となり、そもそも毛色が違う。品質保証の言葉を使うと Verification & Validation でいうところの Validation の部分にあたる。
UX など人間の感覚については、人間を通してしか可視化できない。そのためにユーザーリサーチを実施する必要がある。自動的に実施可能なものはユーザーがどのようにプロダクトを使用しているかなど、データの取得が多く定量分析に適している。 Web やモバイルアプリの文脈ではいわゆるアクセス解析と呼ばれるようなものや、フィードバックを入力させる仕組みなどが具体的な例だ。
習熟すれば、想定通りにユーザーがプロダクトを使うか、それを価値と感じるかなどを間接的に知ることができる。
ただし、思い込みの排除や文脈の補完のために、ユーザーインタビューなどを用いて自動的に
は取得不可能なデータを集めるのが良いだろう。特に最近はパラレルインタラクションを意識したほうが良いため、ユーザーがどのような環境に身を置いているのかは積極的に収集するとよいだろう。
動く仕様書
自動テストにはかなりの情報が詰められている。
- ユースケース
- どう動くのが正しいのか?
- 特にドメイン層の情報が蓄積されることはコアコンピタンスの強化につながる
- 異常系の定義
- どう動いたら正しくないのか?
- 関数の使い方
- 契約による設計などを厳密に適用できない場合でもテストケースで説明できる
情報の信頼性はテストが成功しているかどうかでわかる。
プロダクトやコードベースを理解するためにこれらの情報を使おう。テストケース名は任意の言語で書けるため、ステイクホルダーと一緒に眺めて共通認識を作ることができる。また、チームの新メンバーに自動テストを見てもらうのはオンボーディングとして効率的な手段だ。
テストコードを眺めていると、やりすぎだと感じたり、手薄で不安だと感じるところもあるだろう。こういった感覚は無視するべきでない。普段実行するテストケースを絞ったり、テストケースをブレイクダウンして不安を払拭するなどの活動につなげるとよいだろう。
設計のためのツール
自動テストには TDD を実践するためという位置づけもある。 TDD は次の旨味がある。
- テストを書きやすい = 設計として良さそう
- 次のような、テストが書きづらい状況は得てして良くない状態になっている
- いろいろなものが密結合してしまっている
- 関数やクラスなどの使い方が複雑すぎる
- テストが書きやすいようにコードをリファクタリングすることによって、少なくともコントロール可能な設計がなされていく
- 次のような、テストが書きづらい状況は得てして良くない状態になっている
- 書いたコードが動くことをその場で実感できる
- テストランナーを使用することで即座に結果を得ていく
- ワーキングメモリーに負荷をかけないため、設計にパワーを割ける
- 動くと嬉しくてモチベーションが向上する
- 学習効率が向上する
- 意図通りに動かない場合すぐ気付ける
- 開発効率向上
- テストランナーを使用することで即座に結果を得ていく
即座に TDD を習得するのは難しいが、実践してみる価値はある。まず次の動画で「極める」とどうなるかを見てみると良い。
TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング
自動テストのライフサイクル
自動テストにもライフサイクルがある。書かれてから破棄されるまでの時間を指している。
傾向として、テスト対象が大きいものはライフサイクルが長くなる。ユニットテストはテスト対象と同程度、それより粒度が大きい場合はそれぞれのプロダクトコードよりも長く、 E2E テストに至ってはプロダクトコード全体よりライフサイクルが長くなることもある。
ライフサイクルが長いものはきちんとメンテナンスしなければ混乱をきたすため、テスト対象が大きい自動テストは数を絞っていくべきだ。
テストケース名を書く言語を決める
品質保証や動く仕様書として自動テストを使う場合、考えるべき観点としては次がある。
- プロダクトのステークホルダーが読み書きできるか?
- 論理的に表現できるか?
- 短く書けるか?
チームメンバーは現在だけでなく、未来も含めて考えよう。自動テストは思った以上に長く使えるものだからだ。
論理的な言語として Lojban などが挙げられるが、他の条件にマッチさえすればこういったものも候補として積極的に考えていくべきだ。
CI/CD の前提
ここでは「コードを書いてからユーザーの手元に届くまでに必要なプロセス全般の自動化」という意味合いで CI/CD という言葉を使っている。 CD は Continuous Deployment までを含むと捉えてほしい。
CI の原義や Continuous Delivery と Continuous Deployment の違いなどは次が詳しい。
CI/CD は実質必須と考えた方が良い。というのも、 CI/CD の実際は「並列処理の結果を同期的にまとめる際に発生するタスクの自動化」だ。このタスク量は並列数、つまりチームメンバー数が増えるに従って指数関数的に増えていく。また、本質的でないタスクであるためチームの能力を十全に引き出そうとすると構築が必要となる。
そして自動テストは CI/CD の前提となる。つまり、実質必須で自動テストを書くことになる。
全自動化にこだわらない
整理しながらという前提付きで、自動テストは書けば書くほど良い。省コストで品質が担保可能となり、プロジェクトに関する様々な知識が得られ、開発者の経験値が貯まり、いいことづくめだ。
ただし、自動テスト以外の方法を採用するとコストパフォーマンスが良い場合もある。特に UX 領域は顕著にその傾向がある。ユーザーインタビューの実施もコストが高いため、まずドッグフーディングなど手元で実施できることを検討してみよう。
自動テストに関するチームメンバーの学習コストを現実的な時間で払いきれない、という場合も手動テストやその他で賄って良い。が、少なくとも応急手当であること、できれば今後の予定などはチームで共有しておこう。
戦術
習得
自動テストを書くというのはひとつのスキルだ。習得するために学習や経験が必要となる。その際のトピックとして主に次が挙げられる。網羅されてはいないため、取っ掛かりていどに捉えてほしい。
- 基礎
- 発展
- モックの使い方
- 接合モデル
- UI のテスト
- テストがないコードベースにテストを追加する
- モックの使い方
これだけ多いとハードルが高く感じられるが、まず基礎を確実に抑えよう。発展にあたるトピックはそれぞれ必要になったら学ぶで良い。
習得のハードル
学ぶべきものは明らかで、関連書籍も Web 上の情報も充実しており、テストハーネスも使いやすく整備されている。
それでも、自動テストを書くという行為はハードルが高いようだ。自動テストへの接し方や言動などからの推察になるが、多分次のようなハードルがある。
- 自動テストのメリットを実感していない
- 学ぶ際のフィードバックループが長い
- どう書けば良いのか確信が持てていない
- 学ぶための時間が持てない
これらの解消については環境を整えるのが最も効果的だ。というのも、すべて個人の問題に見えるが、相談可能な範囲に経験者がいないことや、組織に余裕がないことに起因することが多いからだ。例えば 1 番はこの記事を読んでもらえれば理解はできるはずだが、自動テストを書いた経験が少ない方にはそのためのコストまでは実感できず、コスパが評価できない。 4 番に至っては組織目標から整理が必要となる場合もある。
「環境を整える」というとトップダウンでなされるイメージが強いが、最近はチームメンバーそれぞれがリーダーシップを発揮して課題を解決するのが効果的だという話がある。それぞれができることをやっていくことで環境は整うはずだ。
分割統治
一般的なコードと同様、対象が大きい場合、分解してそれぞれへの自動テストを書くと良い。
もちろん、すべてをフラットな構造で書いていくと管理できないため、テストスイートを使ってカテゴライズしていくと良いだろう。次のようなカテゴリーが一般的だろうか。
- テスト対象
- テスト条件
- 引数
また、たいていのテストハーネスでは特定の正規表現にマッチするテストケースのみを実行できる。これを活用して次のように不安定な部分を縮小していくとよいだろう。
- 日中は
[smoke]
タグを持つテストケースのみを実行し、深夜すべてのテストケースを実行する - 実行毎に成功 / 失敗が切り替わるテストケースには
[freaky]
タグを付与し、より小さなテストケースへの分割対象としてチームで共有する
TypeScript のみだが例を用意した。
さらに、設計からのアプローチとしてよく用いられているものには次がある。「部品の完成度を高める」という思想だ。
テスティングトロフィーを参考にする
テスティングトロフィーという概念がある。自動テストの種類とその量についての、ひとつの目安だ。
完全にこれに沿わずとも、どの種類のテストがどの程度必要なのかの認識をチーム内で合わせておくと良い。例えば、次のような分け方がある。
- フォーマッター、型チェック、静的解析
- ユースケースより細かい粒度でのユニットテスト
- ユースケースとして意味のある粒度でのユニットテスト
-
コンバージョンが可能であることを確かめる E2E テスト
- 業種によってはスモークテストと呼ばれることもある
フォーマッター、型チェック、静的解析
書いている言語によっては「コーディング規約」という言葉を聞かなくなった昨今だが、これはいわゆるフォーマッターによる言語レベルでの共通認識が生まれたという理由が大きいだろう。フォーマッターが出力する書き方に沿っているかのチェックも広義の自動テストにあたる。
もちろん、チェックするよりも常にフォーマッターを起動し、適切な書き方からはずれようがない環境にするというのが最も効果的だ。ファイルセーブ時など、コードを書いている最中にフォーマッターを起動するようチームメンバー間で合意しよう。
選択肢があるなら、型チェックや静的解析用のツールは速いものを選ぶと良いだろう。何度も起動したり、 IDE の裏で常に動くものなので、実行速度が開発効率に直結する。
最後に、これらの設定にこだわりすぎないことが重要だ。できるならインストールのみで使えるものやデフォルト設定で足りるものが良い。次の理由による。
- bike-shedding になりがち
- 設定が増えると実行時間も長くなりがち
- 設定が増えることはチームや組織への配慮であり、その組織への過学習につながる
ユースケースより細かい粒度でのユニットテスト
部品としてのコード片の完成度を上げるために使う目的で書いていくと良い。自動テストが意味をなす、できるだけ小さい粒度で書いていく。最小粒度はマクロや関数だろうか。
複数の入力パターンがある場合、それぞれをテストケースとして書いていく。これらを対象の名前を持つテストスイートとしてまとめると管理しやすいだろう。
また、内部状態や実行する時間によって挙動が変わる場合はそれもテストスイートとしてまとめよう。 "In the case of 09:00 - 18:00"
などとできるだけ具体的に書くと良いだろう。
自動テストを置く場所はテスト対象と同じ階層が良い。理由は 2 つ。
- 自動テスト内でテスト対象のコードを読み込む際のパス指定が煩雑になりがち
- テストが書かれているかどうかがディレクトリーツリーからではわかりにくい
次の構成は NG 。
src/
├── __test__/
│ └── domain/
│ ├── entity/
│ │ └── account.test.ts
│ └── usecase/
│ └── authentication.test.ts
└── domain/
├── entity/
│ └── account.ts
└── usecase/
└── authentication.ts
次のような構成を OK とする。
src/
└── domain/
├── entity/
│ ├── account.ts
│ └── account.test.ts
└── usecase/
├── authentication.ts
└── authentication.test.ts
ユースケースとして意味のある粒度でのユニットテスト
プロダクトにとって意味のある論理単位としてユースケースが挙げられる。開発者だけでなくステイクホルダーも含めて挙動や仕様の合意がとりやすい。
また、この粒度のテストが充実していると、意図せずプロダクトを壊していないかをすぐに確かめられて良い。 CI ではこのレベルのテストを常に実行し、フルでの実行は夜間にするという選択肢もある。
できればテストサイズ小で書いていきたいが、意味のないテストを書いても仕方ないので努力目標としておこう。
専用のディレクトリー以下に置くと管理しやすいだろう。テスト対象が複数ファイルにまたがるため、ユニットテストと同様の考え方は使えない。
コンバージョンが可能であることを確かめる E2E テスト
重要度として最も高い。壊れないように細心の注意を払う必要がある。
が、とかく E2E テストは壊れやすい。依存しているものが多く、それらが知らないうちに変更されてしまう確率が高いからだ。常に動くことを保障したいため、これが多いとメンテナンスが大変だ。本当に大切なものに絞るとよい。
こちらも専用のディレクトリー以下に置くべきだ。
フロントエンド特有の事情
フロントエンドは非常に不安定な領域だ。それは主に次の要因による。
- 依存の多さ
- 外界からの影響
- 描画ライブラリーとの密結合
- 価値創出のための探索
依存の多さ
ざっと挙げるだけで次のものがある。
- 描画ライブラリー
- リソース用ライブラリー
- 画像
- 動画
- QR コード
- クラッシュ情報送信
- i18n
- 開発ツール
- テストフレームワーク
- 静的解析ツール
これらすべてに対し、次の観点でチェックしなければならない。
- 意図通りに動くこと
- 判明している脆弱性が含まれていないこと
対応するためにはリグレッションテストを自動化するのが現実的だろう。
外界からの影響
システム外からのデータに対して適切に対処しなければならない。
- 非同期入出力が必要な周辺コンポーネント
- 通信
- ディスク I/O
- ドングル
- 外部 API
- 仕様変更
- 一時的なサービス停止
一般的に腐敗防止層を設けて対処するが、想定していないデータが渡された際の挙動を事前にテストなどで確認しておく必要がある。
スナップショットテスティングの使い方
スナップショットの出力と検証のみであれば書くのが簡単だが、そこから意味のあるテストとするための労力が高く壊れやすいため、普段使いはしない。
複雑なコンポーネントを分割する際や Hunble Object を用いた自動テストを書く際の安全ネットとして一時的に使うなどの用途が良いだろう。
Discussion