導入
どうやら新卒2年目社員のAさんが上司のZさんにプロジェクトにおいてテストコード導入を打診してるようです。少し内容を見てみましょうか。
Aさん(新卒2年目社員)「最近テスト自動化やテストコード、TDDなどの単語をよく聞きます。うちはテストコード書いてないですし、実装後の簡単な動作確認、最終の結合テストしかしていません。開発体験と品質を上げるために、テストコードを導入したいです。」
Zさん(上司)「そうは言うがね、君。今のうちの状況を見てごらんよ。みんな複数のプロジェクトに関わっていて、常に多忙。残業時間もぎりぎりで何とかプロジェクトが回っている状態だよ。そんなみんなにさらに作業を増やすようなことを提案するというのかね?しかも、テストコードはお客様からしたら作っても作らなくても関係ない、いわば直接利益に関係ないような作業じゃないか。もちろん、世の中で認知されているということは知ってるよ?、でも、みんながやっているからって何も考えずうちに導入するわけにはいかないよ。」
Aさん「そう、、、、ですよね。。。。」
Zさん「、、、、、、、ふむ。しかし、せっかく提案してくれたんだ。君はいつも頑張ってくれているから、君の意見を無下にはしたくない。では、私の質問に納得いく回答ができたら、検討することにしようか」
A「ぱぁああ(⊙ꇴ⊙)。ありがとうございます。頑張ります!!」
どうやら、上司の質問に納得いく回答ができたら、テストコード導入を検討してくれるみたいですね。では、実際にどんな質問が来るのか、どんな回答をするのか見てみましょう。
Q1. テストコード導入の効果を分かりやすく説明せよ
Zさん「まず、テストコードを導入することでどんなメリットがあるのかね?当然、工数がかかるということはコストに反映される。つまり、プロジェクト全体の費用が増加するということになる。そのお金を誰が負担するかというと、それはお客様だよね。自分たちにメリットがあるというだけではお客様に説明できないのではないかね?」
Aさん「はい。まずはテストコードが私たち開発者にもたらすメリットについて説明したいと思います。ここでは2点あげたいと思います。
まず、「開発と動作確認の素早いサイクルを構築することで、開発効率、開発体験の向上」です。
私たち開発者はコードを実装した後、常にこのコードはほんとに自分が思った通りに動くのか?という疑問を持っています。この疑問が解消されるのは単体テスト時、つまり実際にコードを動かしたときです。では、コードを動かして確認できるのはいつになるかというと、開発環境に反映して動作確認するときです。ただ、この開発環境は一人に一つ割り当てられているわけではなく、共有の環境です。下手なことをすると他の人に迷惑をかけてしまう、という思いもあるので、実装、とりあえず動かしてみて動作確認、というのが簡単にできません。
テストコードを動かす環境は、必要最低限の環境とするため、そこまでリッチな環境は必要ありません。
例えば、最近だとDockerに代表されるコンテナ技術等を組み合わせてテストコードを実行する環境を構築することで、自分だけの使い捨てのコード動作確認環境を作ることができます。これは、テストコードの対応範囲が単体テスト、結合テストの一部のみを範囲としているためです。テストコードはあくまでも簡易的な動作確認、想定しているロジックが想定している出力を出すのかを確認するための環境として使います。
テストコードをコードを実行する環境としてとらえると、コードを実装、動作確認というサイクルをこれまで以上に迅速に回すことができるようになります。開発者は常に自分のコードは動かしてみるまできちんと動くかどうかは保証できない、と思っています。その間は常に不安ですし、実装したときにこれは確認すべきと思っていても確認が後になればそれを忘れてしまう可能性があります。実装と動作確認をセットで行うことで、より開発が迅速に進み、これまで以上の品質のコードを作ることができるようになります。
」
Zさん「1つ目のメリットはわかった。ただ、テストコードを動かす環境を準備するには結構な工数がかかるのではないか?もちろんどこまでの精度を求めるのかによるが。例えば、コードがDBと接続するコードの場合、どうやってテストコードを書くんだい?」
Aさん「はい。テストコードの役割はあくまで単体テスト+結合テストの一部と割り切ります。なので、DBと接続する必要があるコードのテストコードを書く場合は、実際にDockerでMySQLのコンテナを自分のPC内に構築してそちらを参照させるなどで対応できます。その場合、MySQLの接続先エンドポイントを環境変数から取得するように実装しておき、本番の際は本番のDBへのエンドポイント、開発中はローカルPCのDBのエンドポイントを環境変数に設定することでテスト時に動的に参照先のDBを切り替えることができます。
また、ローカルのPCにDBを構築するまで必要ない、という判断もありだと思います。
テストを実現するライブラリにはコードの処理をMock関数で差し替える機能が提供されている場合があります。(Pythonの場合はunittest.mock --- 入門¶など)
DBからデータを取得するロジックをMock関数で置き換えることで、DB接続のロジック自体のテストはテストコードで実現は出来ませんが、その他のコードのテストコードを書くことはできます。
テスト環境の作成工数が膨大になりそうな場合は、こういった方法で解決できると思います。
また、テスト環境作成の作業は基本的には最初の1回のみです。また、作成した環境はコードで表現されているので、環境を構築した後、他のメンバーはコマンドを実行するだけでテストコードを実行、自分の書いたコードの動作確認ができるようになります。
」
Zさん「ほぉ。環境作成は初回のみコストがかかる、その環境作成もMockを使うことで最小化することができるということだね。では、2つ目のメリットの説明を聞こうか」
Aさん「2つ目は、作成したテストコードは動作確認で使えるだけではなく、コードを修正した場合のリグレッション(後退)の確認としても使えるです。
コードの修正はよく発生すると思いますが、修正したコードが以前と同じ出力をするのかを確認する必要がありますよね。
テストコードがあるとその作業を人手でする必要が無く、自動化できます。
テストコードを実行するだけで、修正した関数のインターフェイス(入力と出力の関係)が変わっていれば、エラーになるので、追加した修正が他のロジックに影響を及ぼす可能性があるということが分かります。
また、コードのプルリクエスト作成時に自動テストを挟むことで、コードのリグレッションに確実に気が付くことができるようになります。テストが通っていない場合は、マージしない、という運用にすることで、既存のコードに影響を及ぼす可能性がある変更を共通のブランチに取り込む可能性を減らすことができます。
プロダクトは生ものだと思っています。常に新機能追加や既存機能のアップデートが必要だと思っているのですが、開発者は常に自分の変更がバグを生まないかに怯えています。その恐怖心から細かく動作確認しがちですですが、その作業を自動化することで、開発者に多大な安心を提供できます。修正した後に大量の動作確認が待っていると考える開発者が自分から機能修正を申し出るでしょうか。
」
Zさん「君ほんとに2年目社員かね???履歴書不正してないよね?考えていることが新卒2年目とはとても思えないのだけれども、、、。わかった。導入することで開発効率と成果物の品質向上に貢献できる可能性があると。さらに開発者にとっての修正の心理的コストを下げる効果があるということだね。納得のいく回答をありがとう。一つ目の質問はクリアだ。」
Q2. テストコード導入で発生するコストをどう考えているのか
Zさん「次はテストコードで発生するコストについて質問しようか。すべてのコードにテストを書く、テストコードも細かく書けばほぼ無限にテストパターンを出すことができるよね?そうなると、実装よりもテストコードを作る方が工数がかかるということにもなりかねない。お客様にとって価値があるのはプロダクションのコードだ。プロダクションコード作成が2日、テストコード作成が3日かかる、というようなことになるとお客様は納得しないと思うよ?」
Aさん「はい。そこはメンバー間で認識合わせをする必要はあると考えています。一つの関数のテストコードを書く場合も、入力データは複数考えられますし、エラーが発生するパターンを無理やり作りだす、などを考えるとテストコードの実装自体が難しくなります。また、本当にしっかりテストコードを書くのであれば、コードのIF条件すべて正しく動くかどうか、という観点でテストコードを書かないといけません。
そこで、私は以下のようなテストコード作成の方針を提案したいと思います。
- テストコード実装の工数は開発工数の40%以内とする(この数字は暫定であり、目安の数字)
- 異常系、正常系のテストを少なくとも1つ以上作成する、ただし異常系はない場合も許容する
- メンバー間で処理ロジックの優先度を認識合わせし、優先度が高いロジックには詳細にテストコードを記載する(例えば、課金処理など)
テストコードの工数を開発工数の○○%とすることで、プロジェクト後の振り返り時に当初の想定とどれくらい乖離しているかを確認することができます。また、明確に数字化することでメンバー間の認識を合わせる効果もあると思います。例えば、40%の場合、自分のプロダクションコードの工数の半分くらいなんだな、という認識で進めることができます。また、必要なテストコード実装に、プロダクションコードの工数の半分以上かかりそうであれば、そのタイミングでマネージャーに相談してどうするかを検討する機会にもなると思います。
テストコードには正常系と異常系が存在しますが、異常系のテストコードは実装が難しく、パターンを上げだすときりがないです。なので、異常系はない場合も許容する方針としています。運用の中で異常系のパターンが発生したタイミングで、そのパターンをテストコードに反映する、という方針も一つの手ではあると思います。
また、テストコードを整備することで、次回の修正時の確認作業の工数を減らすことができます。テストコードがないと今後発生するすべての修正の動作確認を手作業で実施する必要があります。当然工数がかかりますが、テストコードを導入することで単体テストの一部を自動化することができるので、運用コストが下がります。そうなると、お客様にとってもメリットがありますし、早いスピードで機能修正を提供することができます。
」
Zさん「2年目でそこまで考えられているなんて、、、、脱帽だよ、、、。テストコードの工数を開発工数の○○%とすることで、テストコードに費やす工数が無限に増えるのを抑制することができると。さらに、テストコードの環境を整備することで、運用コストを下げられ、機能提供の速度も向上しお客様にとってのメリットもあると。納得したよ、2つ目の質問もクリアだ」
Q3. 品質保証(QA)の作業と重複しないのか
Zさん「次の質問だ。開発後に結合テストを実施すると思うが、その結合テストの内容とテストコードの内容が重複することはないのかね?君はテストコードで単体テストの一部を保証すると説明したね。ということはテストコードで保証されない部分は結合テスト時に手動で確認が必要ということになるだろう。そうなると、結合テストケースを作る際にテストコードをすべて確認して、テストコードに含まれている、含まれていないといったことを選別する作業が必要になるのではないかね。」
Aさん「いいえ。結合テストを作成する際に、単体テスト(テストコード)の内容を把握しないといけない、といったことは起きません。単体テストと結合テストではそもそもターゲットとしているスコープが異なります。
例えば、データ更新機能において、入力値のバリデーションと、DBの更新の二つの処理があった場合を考えます。DBの更新処理のみテストコードが存在し動作が確認されており、バリデーションチェックにバグが存在したとします。単体テストの観点では以下の観点で考えます。
- バリデーションチェックロジックが正しく動くか
- DBの更新処理が正しく動くか
本来は上記2つが単体テスト、つまりテストコードで表現されている必要がありますが、今回はたまたま抜けていたとします。
次に、結合テストの観点では以下の観点でテストします
- 正しい入力の場合はDB更新されるか
- 間違った入力の場合はエラーでレスポンスが返るか
極論、単体テストのケースが抜けていたとしても、そのルートを通る処理が存在しないのであれば、それは表面化しないので、優先度は低いと考えます。最終的にはエンドツーエンドテスト(以下E2Eテスト)でテストをパスすることができれば、ユーザが使う分には問題ないといえます。例えば、UI的に数字しか入力できなくなっている場合、ある処理に数字以外が入る可能性は低いです。もちろん、テストコードで数字以外が入った場合は正しくエラーを返す、というテストコードを書ければ理想ですが、優先度が低い部分のテストコードをどこまでしっかり書くのか、という問題があります。今回は数字以外が入る可能性は低いので、数字以外が入力として入るテストケースは書かないとしても、特に上位のテストには影響しません。つまり、上位のテスト(結合テスト、E2Eテスト)では、下位のテスト範囲を意識しなくても問題ないと思っています。ただし、上位のテストでエラーになったパターンを下位のテストケースに追加するということは重要です。
補足ですが、単体テストが最も費用対効果が高く、よりたくさんのテストケースを準備する必要がある、という部分はチームで認識を合わせる必要があると思っています。例えば、単体テストで「バリデーションチェックロジックが正しく動くか」のテストコードを書いたとします。このテストコードはほぼどんな環境でも普遍的に効果を発揮するもので、一度テストケースを作成するとそのあとずっと使えるので費用対効果が高いです。
ただ、「正しい入力の場合はDB更新されるか」の場合、内部のビジネスロジックに変更が入ると、従来のテストとは異なる値が返ってくる可能性があります。つまり、上位のテストになればなるほど、変化に対して脆弱、つまり費用対効果が薄れる、ということになります。
なので、単体テスト、結合テスト、E2Eテストそれぞれ下位のテストケースを意識する必要はないですが、それぞれのテスト部分を担当する人がそれぞれのフェーズに求められている内容を理解してテストケースを作る必要があります。また、それぞれのフェーズでテストする観点が違うので、作業の重複は発生しないと考えています。
」
Zさん「ほぉ。テストピラミッドの考え方もしっかり理解しているのか。君独学で勉強しているの?そんな知識どこから得ているの?君人生2週目の人じゃないよね?、、、、、、。よし、この質問も合格だ」
参考リンク:
テストのピラミッドを開発者と一緒に眺めてみよう!
Q4. メンテナンスについて
Zさん「これが最後の質問だ。メンテナンスについてどう考えているかを聞かせてもらおう。プロダクトには運用・保守という作業が発生するのは理解しているね。テストコードを導入することでプロダクションのコードに加えて、テストコードも保守しないといけなくなる、そうなると保守工数が逆に増える、といったことは発生しないのかね?また、プルリクエスト作成時にテストが通るかどうかをチェックするといったが、小さい修正でテストがエラーになり、テストコードの修正が大変な状態だと、開発体験が逆に低下するということも考えられると思うよ。」
Aさん「はい。確かにコードの一部分を修正してテストコードのあらゆる部分でエラーが起きる、という可能性もあります。そのために、テストコード作成時にいくつか気を付ける必要があると思っています。
一つ目:テストコード間でデータを共有しない
例えば、テストコードでローカルPCのMySQLに接続する場合、テーブルデータを複数のテストコードで共有すると、あるロジックを修正してDBのデータが変更されると別のテストコードでエラーが起き、エラーの特定が難しくなります。テスト単位でテーブルをドロップするのは効率が悪いので、参照するデータレベルで同じデータは参照しないようにする、などの工夫・ルールが必要だと思います。
二つ目:ロジック内部に依存するようなテストは書かない、入力と出力に着目したテスト(もしくはその関数のメイン処理にのみ着目したテスト)を書く
例えば、日時のタイムゾーンを変換する処理のテストコードを書く場合、内部で使っているライブラリがインストールされているかどうか、といったテストは不要で、入力に対して期待する結果が返るかどうかのみを対象とします。また、「DBにデータを書き込む」という処理をテストする場合、DBに書き込まれたかどうかだけをチェックし、書き込まれた値については詳細にチェックしません(これは場合によりますが、、)。
これらの方針をメンバーで共有することで、コードを変更したときに関係ないテストコードでエラーが発生し、開発体験が低下するというケースを減らすことができると考えています。
また、テストコードの保守工数が増える件については、テストコードの保守工数が増える分を、プロダクションコードの修正に対しての手動で単体テストを実施する場合の工数と相殺できるものだと考えています。相殺できるだけでなく、テストコードの実装を自動化することで、手動テストと比較すると品質の高い動作確認がいつでも、自動で実行されるというメリットを受けることができます。
」
Zさん「なるほど。プルリクエスト前にテストコードの実行を自動化することで、テストコードのメンテナンスを必須化すると。さらに、テストコード作成を工夫することで、プロダクションコードの修正で発生するテストコードの修正を最小限に抑えることができ、保守工数をできるだけ減らせると。また、動作確認を手動で実行する必要がないので、テストコードの保守工数は手動で動作確認する場合の工数と相殺されると。」
結果発表
Aさん「質問にすべて回答しました。結果はどうでしょうか?」
Zさん「そこまでしっかりと考えられているのであれば、テストコードをプロジェクトに導入してみようか。では、テストコードの導入はA君がリーダーとして主導してほしい、期待しているよ」
Aさん「はい!ありがとうございますヽ(^^)ノ」
最後に
今回の話はフィクションなのですが、テストコードの導入のハードルが高い現場はたくさんあると思います。使う言語やフレームワークによっても考え方が変わると思います。作るシステムによっても。それでも、今回のAさんみたいに諦めずに提案してみると、新しい道が開けるかもしれません。
この記事がテストコード導入に悩まれているあなたの、第一歩を後押しするような内容であれば幸いです。