時間がないからこそ、テストを書く
こんにちは。
株式会社ココナラ在籍のKです。
「時間がないからテストは後で書く」
そのような言葉を聞くたび、「テストを一緒に書くことでむしろ時間を節約できるのに、もったいない」と感じます。
本記事では、その理由を明確にした上で、私がよくやっているTDDをゆるく取り入れたテストの進め方をご紹介します。
対象読者
本記事は、以下のような悩みをお持ちの方に向けた記事です。
- テストの重要性は理解しているものの、時間的な制約からテストを後回しにしてしまいがち
- TDDに興味はあるものの、難しそうでなかなか実践できない
- TDDのテストファーストという手法に馴染めない
- チーム内にテストの文化を広めたい
本記事の構成
大きく以下の2つの構成になっています。
- テストを後で書くという考え方への考察
- TDDをゆるく取り入れた実践手法
本記事におけるテストの定義
本記事で扱うテストは、主としてロジックのユニットテスト、インテグレーションテストを指します。
画面のE2Eテストなどは、その性質上、実装後でなければ記述できないため、ここでは対象外とします。
テストを後で書くという考え方への考察
「時間がないからテストは後で書く」という考え方は、以下のような認識に基づいているのではないかと思います。
- テストを書かないことで、より早く作業を進めることができる
- スケジュール上のクリティカルパスを短くすることができる
しかしながら、一方でテストを書かないことによるマイナス面の影響もあります。
余計にかかる時間
テストを書く時間は省けるものの、以下のような時間が余計に発生します。
余計にかかる時間: マージ前
手動実行時間
テストを書かない場合、手動で画面操作やAPI実行を行いながら動作確認する必要があります。
これは効率が悪いです。
実際、「時間がない」と言いながら画面操作に時間を費やしている人を何度も見てきました。
テストがあれば、コードを保存するたびに自動実行でき、手間を大幅に削減できます。
この手動実行時間は個人のスキル向上に一切寄与しない使い捨ての時間なので、もったいないです。
手動実行にかかる時間
コードレビュー時間
テストコードは、コードの仕様や使用方法を端的に表現する役割も担っています。
テストコードがない場合、レビュアーに対して仕様や使用方法を別の方法(例: Pull Requestの説明に詳細な仕様を書く)で伝える必要があり、その分の時間が余計にかかってしまいます。
また、自身だけでなく、レビュアーにも以下のような負担を強いることになります。
レビュアーの負担
- テストコードなしで仕様や使用方法を理解しなければならない
- コードが正しく動作するかという観点でもレビューを行わなければならない
- レビュー依頼者が確認済みだと言っていても、どこまで確認しているか分からない
- 正常に動作しなくても自分の責任ではないと割り切るにしても、不安を感じながらレビューをすることになる
コードレビューはレビュアーにとって負担の大きい作業です。
最大限の配慮を心がけたいところです。
余計にかかる時間: マージ後
ケースの考慮漏れによる手戻り時間
テストを書かない場合、考慮すべきケースが漏れやすくなります。
例えば、実装中は見落としていたケースも、テーブルドリブンテストで網羅的に検討するなかで発見できることは少なくありません。
この考慮漏れに気付く機会は、テストを書くことで得られるものです。
考慮漏れがQA(品質保証)やリリース後の段階で見つかった場合、調査や対応に多くの時間を費やすことになるので、もったいないです。
テスタビリティの確保にかかる時間
また、あとでテストを書こうとした際に、テスタビリティを確保するために実コードに手を加える必要が生じることがあります。
すでに動いているコードを修正する場合、神経も使いますし、再テストなどの余計な時間もかかります。
この時間は最初からテストを書いていれば発生しなかった時間なので、もったいないです。
リファクタリングにかかる時間
コードは書く時間よりも読まれる時間の方が圧倒的に長く、特に中長期的にメンテナンスされるコードであれば、リファクタリングは必要不可欠です。
しかしながら、テストがない状態でのリファクタリングは非常に困難で、多大な時間がかかります。
かといって、リファクタリングしないと、コードの修正や障害調査のたびに余計な時間を費やすことになります。
時間以外に失われるもの
さらに、時間以外にも失われるものがあります。
テストコード
「後で書く」の本当の意味
「テストコードは後で書く」という言葉は、実際には「(余裕があれば)書く」という意味で使われることが多いです。
しかしながら、開発現場で余裕がある状況は稀なので、したがってテストが後で書かれることも稀です。
マージ後の制御不能性
コードがひとたびマージされると、それは大規模なコードベースの一部となり、個々の変更内容には注意が払われにくくなります。
水槽の中の傷ついた魚を見つけるのは容易ですが、ひとたび川に放流してしまえば、その傷を見つけ出すのは困難になり、顧みられることがなくなるのと同じです。
Pull Requestで管理されている状態
大規模なコードベースに統合された状態
スキル向上の機会
テストを後回しにすることは、プログラミングスキル、特にテストに関する以下のスキルの向上を阻害します。
-
テスト設計/コード記述能力
- どのようなテストケースが必要か考える機会が減り、効果的なテストを設計する能力が育たなくなる
- また、テストを書く頻度が少なくなり、テストフレームワークの使い方やテストの書き方に習熟する機会を失う
- 結果として、テストを書くのに時間がかかり、テスト自体が億劫になってしまう悪循環に陥る
-
バグ検出能力
- テストを通じてバグを発見し修正する経験が不足するため、バグの原因特定や予防スキルが向上しにくくなる
- 仮にQA段階でバグに気付いたとしても、フィードバックサイクルが長いため、実装中にその可能性に気付けなかった理由を振り返るのが難しくなる
-
コード品質への意識
- コードの品質を客観的に評価する機会が減り、品質の高いコードを書くという意識が希薄になる可能性がある
テストを後回しにすることは、短期的な効率を優先するあまり、エンジニアとしての長期的な成長機会を自ら放棄することに等しいと言えます。
テストを書く時間は自身の力で短縮可能
上記のようなデメリットはあったとしても、以下のような疑問が出てきます。
- 結局テストを書く時間は増えるのでは?
- テストを書くのが苦手で時間がかかる
これらの課題は適切な学習と実践によってある程度克服可能です。
例えば、テストの基本的な書き方や設計原則、DI、テーブルドリブンテストなどの手法を学ぶことで、テストコードの記述に要する時間を大幅に短縮できます。
学習の足がかりとして、テストに関する良書が多く存在します。
例えば、『Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考』は、テストについて分かりやすく解説しており、入門書として優れています。
TDDをゆるく取り入れた実践手法
ここからは、筆者がよくやっているテストの進め方を説明したいと思います。
これは、筆者と筆者が置かれている状況において、汎用的に使いやすい方法であって、すべての人に当てはまるわけではありません。
TDDとは?
TDDとは、テスト駆動開発の略で、プログラミング手法のひとつです。
高品質なソフトウェアを効率的に開発することを目的としています。
TDDでは、以下のような流れで開発を行います。
- 最初にテストリスト(期待される振る舞いのリスト)を作成する
- このプロセスの存在により、テストケースの網羅性が高くなる
- Red/Green/Refactorサイクルを繰り返す
- 🔴 Red: テストリストの中から1ケースを選択し、テストを書く
- テストを書く過程でインタフェースやモジュールの役割を考える
- テストが書きやすいように設計することで、凝集度が高く、疎結合な変更に強い設計になりやすくなる
- 🟢 Green: テストが通るように実装する
- 必要最低限のコードでテストをパスさせることで、小さなステップで確実に進捗できる
- 🔧 Refactor: 必要に応じて、リファクタリングする
- テストによって安全性が担保された状態で、コードの重複排除や可読性の向上などのリファクタリングを行うことができる
- 🔴 Red: テストリストの中から1ケースを選択し、テストを書く
このサイクルを繰り返すことで、徐々に設計や実装を洗練させていくのがTDDです。
さらに詳細を知りたい方は、以下の記事が参考になると思います。
特徴: ゆるいTDDとCDDの組み合わせ
筆者のテスト手法は、TDD(テスト駆動開発)の利点を部分的に取り入れつつ、コメントを最初に書く手法を組み合わせたものです。
以下、「コメントを最初に書く手法」をCDD(コメント起動開発)と呼びます。
CDDという名称は以下の記事から拝借しました。
また、CDDという名前は付けられていませんが、このコメントを最初に書く手法は『A Philosophy of Software Design』でも言及されています。
TDDの難しさ
TDDはテストにより設計を駆動する優れた手法ですが、厳密に運用するには以下のような難しさがあると感じています。
- テストから考え始めるのが直感的ではなく、やりづらさを感じる
- 設計の全体像がまだ見えていない段階でテストを書くのは、経験と勘が求められる
- テスタビリティを優先するあまり、本来自然な設計から逸脱し、設計が歪められることがある
- ユニットテストを徹底するためにモックを多用しがちで、その結果、テストが実装の詳細に密結合してリファクタリングが困難になったり、モックの定義に手間がかかったりする
- 本来、TDDの手法とモックの使用頻度は別の問題だが、TDDに慣れていない場合、モックが増えがちではある
これらはTDDに習熟することで克服可能ですが、初心者にとっては敷居が高いです。
また、特定のケース(例えば、インタフェースが未確定なモジュール)では適用が難しい場合があります。
TDDの優れている点
一方で、TDDの以下の点は優れていると感じています。
- テストリストにより、テストケースを網羅的に考えることができる
- Red/Green/Refactorサイクルにより、安心感と効率の良さが得られる
- テストが書きやすい設計になるように強制されるため、凝集度が高く、疎結合な変更に強い設計になりやすい
コードを書く流れ
そこで、TDDの難しさを緩和しつつ、優れている点を享受するために以下のような進め方をしています。
-
実コードから書き始める
-
(1) ここで、モジュールのインタフェースを考える
function getPost(postId: string): Post | undefined { }
-
-
TDDのテストリストにあたるもの
-
(1) 何をすべきか(期待される振る舞い)を明確にするためにコメントを書く(📝)
function getPost(postId: string): Post | undefined { // APIで投稿を取得する // ネットワークエラーが発生した場合は例外をスローする // 投稿が存在しない場合は、undefinedを返す // 投稿が存在する場合は、取得したデータを返す }
-
-
TDDのRed/Green/Refactorにあたるもの
-
(1) コメントの下に処理を記述していく
function getPost(postId: string): Post | undefined { // APIで投稿を取得する const res = client.fetchPost(postId); // ネットワークエラーが発生した場合は例外をスローする // 投稿が存在しない場合は、undefinedを返す if (res.status === 404) { return undefined; } // 投稿が存在する場合は、取得したデータを返す if (res.status === 200) { return res.post; } }
-
(2) ある程度実装が進んだ段階でテストを1ケース書き、実行する(🔴 or 🟢)
- 「ある程度」の目安(あくまで目安として捉え、柔軟に判断してください)
- ビルドが通る状態になった
- 正常パターンが実装できた
- 動作確認が必要になった(ちょっと動かしてみたくなった)
- 「ある程度」の目安(あくまで目安として捉え、柔軟に判断してください)
-
(3) テストが通らない場合は、実コードを修正する(🔴 -> 🟢)
-
(4) テストケースの網羅性を検討し、漏れがあればコメントとして追加する(📝)
- 例えば、
500
エラーのケースを追加する
- 例えば、
-
(5) 必要に応じてリファクタリングし(モジュールのインタフェースの見直し含む)、コードを整える(🟢 -> 🔧 -> 🟢)
-
(6) 以上の手順を繰り返す
-
-
コメントを整える(📝)
- 最終的に実コードが自己説明的で、元のコメントが冗長になった場合は削除する
- 役に立つコメントの場合はそのまま残す
コメントを先に書く理由
コメントを先に書いているのは以下の理由からです。
テストケースをある程度網羅的に検討できる
実装前にコメントで処理内容や分岐を記述することで、テストケースを網羅的に検討するきっかけになります。
- テストリストほど網羅性は高くないものの、一定テストリストの役割を果たす
- 実装中に思いついたケースを忘れないようにするためのメモとしても機能する
Red/Green/Refactorサイクルのメリットを享受できる
TDDとテストを書き始めるタイミングが異なるだけなので、Red/Green/Refactorサイクルの安心感やリファクタリングしやすいというメリットを享受できます。
設計に集中できる
APIのようにインタフェースの形式が固定されている場合はテストから書くのも容易です。
しかしながら、より細かいモジュール単位では、モジュールのインタフェース設計も同時に行う必要があり、テストを書きながら設計を考えるのは筆者にとっては直感的ではありません。
コメントを先に書くことで、まずは設計に集中し、テストケースの網羅性は後で考えるという段階的なアプローチを取ることができます。
モックの使用を必要最低限に抑えることができる
コメントはただのコメントなので、モックがなくても自由に書けます。
また、実コードから書き始めるため、テストのために設計が歪められる可能性を低減できます。
TDD vs ゆるいTDDとCDDの組み合わせ手法の比較
最後に、ここまでの内容を表にまとめると、以下のようになります。
項目 | TDD | ゆるいTDD + CDD |
---|---|---|
テストコード記述のタイミング | 最初(テストファースト) | 実コードをある程度書いてから |
サイクル | テストリスト、 Red/Green/Refactor |
コメント、 Red/Green/Refactor |
テストケースの網羅性 | ✅️ テストリストを作成することで、テストケースを網羅的に洗い出すことができる |
🔺 実装を進めながらテストケースを適宜追加していくため、TDDに比べると網羅性はやや劣る |
設計への影響 | ✅️ 凝集度が高く、疎結合で変更に強い設計になりやすい |
🔺 テストファーストではないため、TDDに比べると劣る |
人間の認知との適合性 | ❌️ テストから考える (非直感的) ※特にTDDに慣れていない場合 |
✅️ 設計から考える (直感的) |
モックの使用頻度 | ❌️ モックが増えがち ※特にTDDに慣れていない場合 |
✅️ モックが最低限ですむ |
リファクタリング | ✅️ プロセスに組み込まれている |
✅️ プロセスに組み込まれている |
まとめ
私にとっては、「テストを先に書く」のはやりづらく、「テストを後から書く」のも効率が悪いです。
私には、「テストを並行して書く」、つまり壁打ちをするように実コードを書くのと並行してテストコードを書くスタイルが合っています。
「時間がないからテストを後で書く」のではなく、「時間がないからこそ、テストを並行して書く」のです。
ぜひ、みなさんも自身にとって最適なやり方を模索してみてください。
ココナラでは積極的にエンジニアを採用しています。
現在、フルサイクルエンジニアを絶賛募集中です。
カジュアル面談で、ココナラの魅力やキャリアパスについて詳しく聞いてみませんか?
採用情報はこちら。
その他のエンジニアの方も募集しています。
カジュアル面談希望の方はこちら。
Discussion