はじめに
「テストコードを書きましょう」とはよく言われるし、テストコードが大事だってことも理解できるんだけど、何をテストしたらいいの?どんなテストを書いたらいいの?と迷っている初心者プログラマさんは意外と多いのではないでしょうか?
そんな方たちに向けて、この記事では僕が普段意識しているテストコードの方針を紹介します。
おことわり
本来であれば具体的なコード例も豊富に入れたいところなのですが、かなり時間がかかってしまうので、いったん文章メインで記事を公開します。
もしかすると、そのうちコード例も一緒に盛り込んだ「リッチバージョン」を公開するかもしれません。
この記事の前提条件
この記事ではあくまで、「今現在、筆者が仕事で書いているテストコードの方針」です。
そのため、状況が異なると適用しづらい方針も出てくるかもしれません。
筆者は以下のような現場でコードを書いています。
- 月額定額で、お客様と毎週定例ミーティングを開きながら、開発とリリースを繰り返す開発スタイル(弊社ソニックガーデンで実践している「納品のない受託開発」)
- 開発メンバーは少数(最小で1名)
- 不具合が出た場合は、すぐに修正してすぐにリリース可能
- 基本的にドキュメントは書かない
- 使用するメインのWebアプリケーションフレームワーク/テスティングフレームワークは、Ruby on RailsとRSpec
前提条件が異なるために適用しづらい方針が出てきたら、ご自身の状況に合わせて適宜テーラリング(カスタマイズ)してください。
用語の定義
本文中に出てくる「アプリ側のコード」というのは、アプリケーションにおける「テストコード以外のコード」を指します。
「本番コード」や「productionコード」と呼ばれることもあります。
(スマートフォンアプリの「アプリ」のことではありません。)
Railsであればappディレクトリやlibディレクトリ、configディレクトリ等に配置されるコードが「アプリ側のコード」に該当します。
戦略面
なぜテストを書くのか、どういうときにテストを書くのか
- テストコードはアプリケーションの命綱、安全ネット、防弾チョッキ
- 「このコード、テストなしでリリースするのはちょっと不安だな」と思ったら、それがテストを書くトリガー
- リリースするときに「ちゃんとうまく動きますように」と祈ってる自分がいたら、テストが不足している証拠
- 将来の自分が楽をするために書く
- コマンド一発でこれまで書いてきたコードの動作確認ができる! 速いし、楽ちん!!
- 毎回リリース前に全部手作業と目視でテストするつもり?無理だよね!
- 将来、自分のコードをメンテするかもしれない他のメンバーのために書く
- ドキュメントを書く代わりに、コードを書いた人の意図や頭の中にある仕様を明示的なテストコードとして残しておく
- 将来、Railsやライブラリ(gem)をアップデートするときのために書く
- 「テストが全部パスすればきっと大丈夫」と思えるようにする
- 不具合を修正するときに書く
- テストコード上で不具合を再現させて、失敗するテストコードがパスするようにアプリ側のコードを修正する
- テストコードがあれば不具合の修正と再発防止を一度に実現できる(同じ不具合を再発させてしまうのはプロとして恥ずかしい)
テストコードの対象になるコード
- 今の自分、または将来の自分が信頼できないところ
- 将来、コードの他の部分を変更したときに、不具合が発生しそうな予感がするところ
- セキュリティ上、重要なところ
- 権限管理など
- 不具合があると致命的なところ
- 課金や決済など、お金を扱うところ
- メールの送信先
- そのシステムの重要度の高いユースケース
- ネット販売サービスであれば、「ユーザー登録して、商品を選択して、購入」という一連の流れがそれに該当する
- 複雑なロジックやトリッキーなコードを書いてしまったところ
- シンプルで美しいコードを書くのがベストだが、往々にして理想と現実は異なる場合もある
- やむを得ずモンキーパッチを当てたところや、問題回避のために無理矢理なハックをしたところ、非公開APIを利用して実装したところ
- Railsやライブラリのバージョンアップで動かなくなる恐れがある
- 手作業で何度もテストするより、自動化した方が明らかに速いところ
- アカウント作成時の確認メール送信(手作業で何度も新規アカウントを作るのはしんどい)や、システム日時に依存するロジックなど
- 例外処理
- 例外処理のバグでさらに例外が発生すると、元の例外情報が失われてしまう
- 例外を発生させづらい場合はモックを使ってわざと例外を発生させる
テストコードの対象にならないコード
そもそもの話として、無理にカバレッジ100%を目指す必要はない。
価値の低いテストコードは開発効率を下げる原因になる(テストの実行が遅くなったり、仕様変更時のテストコード修正が大量発生したりする)。
たとえば、以下のような場合はテストコードを書かなくてもよい。
- (Railsの場合)モデルのシンプルな関連(
belongs_to
やhas_many
)や、シンプルなバリデーション- ロジックを書くというよりも、フレームワークの設定だけで済む部分
- アプリケーション上の基礎的な部分なので、他のテストコードを書いているうちに自然とテストできている(万一、関連やバリデーションがおかしくなっても気づける)ことが多い
- ただし、「シンプルでない」ならテストコードを書く(特殊な外部キーで関連している、凝った正規表現で入力値チェックをしている、等)
あわせて読みたい👇
- 自動生成されて、自分で何も手を加えていないコード
- ただし、生成直後は問題なくても、将来のライブラリのバージョンアップ時に問題が起きる可能性もある点には留意が必要
- すでに別のテストで十分動作確認できているロジック
- 例: フィーチャスペック(いわゆるE2Eテスト)を書くとコントローラのロジックを通るので、コントローラスペックは省略する、など
- 最悪、不具合が出ても致命的ではないところ
- 問題が出たらすぐに修正リリースする
いったん後回しにできるケース
以下のようなケースではテストコードを後回しにしてもよい。
ただし、いずれも「テストを書かなくてもよい」ではないので、あとで忘れずにテストを書くこと・・・と言っても、現実にはうやむやになりがちなので、後回しはなるべく避けたい。
- 仕様がまだ固まっていない場合
- 本当にリリースを急いでいる場合
自動テストを諦めるケース
テストコードも一種のプログラムである。
テスト対象となるアプリ側のコードの技術要素や、テストコードを書くプログラマのスキルによっては、「技術的な制約により、テストコードが書けない」というケースもありうる。
その場合は、やむを得ず自動テストを諦めて手動テストを採用してもよい。
ただし、手動テストを採用する場合でも以下の点に気を付けたい。
- 本当に自動化する方法がないのかどうか、周りの同僚に確認・相談する
- リリース前は必ず手動テストを実施する
- テストを自動化できていない箇所を明文化する(または何らかの方法で、手動テストが必要であることを他のメンバーがわかるようにする)
戦術面
テストコードを書くときに意識すべきこと
- ヌケ・モレのない、必要最小限のテスト項目を抽出する
- 閾値の境界をテストする、条件分岐を全網羅する、デシジョンテーブルを使ってテスト条件の組み合わせを検討する、etc
- こうした考え方、つまり一般的なテスト技法は自動テストも手動テストも同じ
- テスト技法について勉強したことがなければ、テスト技法に関する書籍(「はじめて学ぶソフトウェアのテスト技法」など)を一冊読む
- データの更新や削除を伴うテストを書く場合は、before/afterの両方を検証する。(afterだけ検証して満足しない)
- 更新なら、AがBに変わったことを検証する(つまり、最初はAだったことを検証しておく)
- 削除なら、Aが消えたことを検証する (つまり、最初はAが存在していたことを検証しておく)
- afterだけ検証するテストコードは「最初からBで、なおかつ何も更新できていなかった可能性」や「最初からAが存在せず、なおかつ削除はできていなかった可能性」を否定できない
- 上から下へ、素直に読み下せるテストコードを書く
- 過度なDRYを目指すのはNG
- 頭の中で変数を何度も置換したり、視線が頻繁に上下するテストコードは「臭う」テストコード
-
let
やsubject
、shared_examples
といった、テスティングフレームワーク特有の機能を多用するのは黄色信号(ご利用は計画的に)
- テストコード全体がなるべく1画面に収まるようにする
-
describe
やcontext
のネストを深くしすぎると、画面のはるか上の方でlet
やbefore
が登場したりするので要注意
-
- 複雑なテストコードのデメリットを理解する
- 複雑なテストコード = 過度にDRYなテストコードや、if文やループ処理が頻繁に登場するテストコード、テスティングフレームワークの機能を多用しすぎるコード、など
- テストコード自体がロジカルになり、「テストコードのバグ」が発生する。「テストコードのテスト」が必要になるのは本末転倒。
- テストコードのロジックにバグがあると誤検知が発生する。本来失敗すべきテストがパスしたりするのはテストとして致命的。なので、ロジカルなテストコードは避ける。
- 第三者が見たときに、対象のロジックがどんな仕様なのかぱっと理解できなくなる。
- アプリ側のコードを見て仕様がぱっとわからなかったのでテストコードを見てみたのに、テストコードを読んでも意味がわからない、といった状況になると悲惨。
- DRYを捨てた結果、仕様変更が発生したときに大量のテストが壊れて修正が必要になる問題はある程度許容する
- そういった問題が頻発し、許容できなくなったタイミングでDRYにすることを検討する
- テストコードのコーディングルールを厳しくしすぎない
- 「必ず
subject
を使う」「it
の中のexpect
は必ず1つだけにする」といった制約を設けない - 制約が厳しいと、ひねくれたテストコードが出来上がったり、テストコードを書くのにやたらと時間がかかったりする
- コーディングルールに沿ったテストコードを書くために頭を悩ますぐらいなら、自由にテストコードを書いて、どんどんアプリ側のコードの実装を進めた方が良い
- 「必ず
- 実際のユースケースが想像できるテストデータを用意する
- 「あああ」とか「テスト1」ではなく、「西脇太郎」や「株式会社イグザンプルドットコム」にする
- 新しくプロジェクトに参加したメンバーにとっては、リアルなテストデータの方が圧倒的に仕様がわかりやすい
- ユーザの名前付けに迷ったらアリスとボブに登場してもらう
- いつでも、どこでも、誰がどんな順番で何度実行しても、パスするテストを書く
- 実行順序や実行環境、データベース上のid、システム日時といった要素に依存したテストを書くのはNG
- privateメソッドは直接テストしない
- テストしたいprivateメソッドを通るような条件下で、publicメソッドをテストする
- AAA(Arrange、Act、Assert)を意識したテストコードを書く
- Arrange(準備)、Act(実行)、Assert(検証)
- 詳しくはこちらの記事を参照
TDDで開発するかどうかについて
- テスト駆動開発(TDD)は以下の条件に合致した場合に実施する(無理にTDDで開発しなくても良い)
- これから作るプログラムの入力と出力の仕様が明確である
- なおかつ、テストコードの書き方がぱっと頭の中に思い浮かぶ
- 上の条件に合致しなければ、TDDを諦めて「アプリ側のコードを書いて、あとからテストを書く」でもよい
- ただし、その場合は「コードを書く → テストを書く → テストがパスする → 終わり」ではなく、わざとテストコードを壊して期待どおりにテストが失敗することも確認すること
- これは「テストの誤検知(false positive)」を回避するためである
- コードから先に書いた場合、うっかり不適切なテストコードを書いてしまい、失敗すべきテストも成功と判定してしまう問題がよく起きる。常にパスするテストはテストとして役に立たない
- 別のアプローチとして、「実験用のコード(スパイク)を書く → 入出力の仕様が明確になった時点でスパイクを破棄する → TDDで正式なコードを書く」という方法もある
アプリ側のコードについて
- アプリ側のコードも「テストの書きやすさ」を意識して設計・実装する
- 例: モックを差し込みやすいようにあえてメソッドを分ける、DOMを取得しやすくするためにviewのHTMLにclass属性やid属性を追加する、rakeタスクに直接ロジックを書くのではなく、メインロジックはモデルに書いてrakeはモデルのメソッドを呼び出すだけにする、等
- テストが書きにくいと思ったら、アプリ側のコードを変更することも検討する
手動テストの必要性について
テストを自動化したからといって、手動テストが全く不要になるわけではない。
以下のようなケースではローカルマシン、またはステージング環境で手動テストを実施すること。
- 画面上の見た目
- 自動テストが全部パスしていても、画面の表示が崩れていたりすることがある
- 外部リソース(Amazon S3や外部APIなど)と連携する部分
- 自動テストではローカルストレージやモックでしかテストできないことが多い
- そのため、自動テストがパスしていても、本番環境で正常に動く保証はない
- 何らかの理由で自動テストを諦めたロジック
- 当然、自動テストではテストできていないため、手動テストが必要になる
- Simplecovのようなカバレッジ計測ツールでテストカバレッジを調べると、自動テストでカバーできていないロジックを確実に見つけ出すことができる
- そのシステムの重要度の高いユースケース
- Railsのメジャーバージョンアップなど、大きな変更を加えた後に実施する
- 「動いて当たり前」の機能に万一不具合が発生すると致命的であるため
その他
テストコードのコードレビュー
- Pull Requestをレビューする際は、アプリ側のコードの変更内容に対して、テストコードが適切に追加/修正されていることを確認する
- 明らかにテストコードが不足している場合はその旨コメントする
- 加えて、上記の「テストコードを書くときに意識すべきこと」で挙げた観点でもテストコードをレビューする
- 怪しい点を見つけた場合はその旨コメントする
- コードレビューする際は自分のことは棚に上げて良い
- 「これ、僕もできてないからな・・・」と指摘を躊躇するのはNG
テストを書くスピード(手の速さ)について
- テストがスラスラ書けるようになるまでは時間がかかることを覚悟する
- RSpecに限らず、テスティングフレームワークには多かれ少なかれクセがある
- 慣れないうちは自分のやりたいことをどうやってテストコードとして表現したらいいかわからなかったり、テストコードが意図したとおりに動いてくれなかったりすることがよくありがち(僕もそうだった)
- 経験を積めば、イライラせずに落ち着いてテストが書けるようになるからがんばって!
- ハマったら、テストが得意な先輩や同僚に助けを求めよう(テストで何時間もハマるのは時間がもったいない)
- Railsプログラマであれば「Everyday Rails - RSpecによるRailsテスト入門」を読むのもおすすめ(翻訳者である僕自身もお世話になった)
なぜかパスしないRSpecのデバッグがようやく完了。RSpecを書き慣れてる僕でもハマるときはハマる。が、絶望的な気持ちになることはなくて、「どこかに原因があるはず。絶対パスするはず」と思いながら粘り強くデバッグできるのは今まで何度も動かないスペックをパスさせてきた経験があるからかなー😎
— Junichi Ito (伊藤淳一) (@jnchito) April 18, 2018
テストの実行スピードについて
- システムの規模が大きくなったり、テストの量が増えたりすると、テストが遅くなるのは仕方がない。自然の摂理としていったん諦める
- 「遅い」と判断する基準や、遅いテストの原因と対策は現場によって異なり一般化しづらいため、高速化のテクニックについてはここでは説明しない(他のネット記事等を参照してください)
- ただし、原則として早すぎる最適化は避ける(上に書いた「複雑なテストコード」と同じ問題が起きかねない)
- プロジェクトの初期は愚直で素直なテストコードを書く。許容しづらい遅さになってきたら、そのときにボトルネックを調査し、対策を考える
他人の書いたテストコードが読みづらい場合
(2018.6.14 追記)
先日、@suinさんが以下のようなアンケートをTwitterで実施していました。
僕が携わるプロジェクトでは他人の書いたテストコードが読みずらい問題があるのですが、皆さんのプロジェクトではどうですか?
— ❄️suin (@suin) June 12, 2018
チームでテストコードのリーダビリティを高めるのにどんな取り組みをしてますか?よければ教えてください。
僕は「うちは読みやすいテストコードになってる」を選択したのですが、アンケート結果を見る限り「うちもテストコードが読みにくいので改善したい」というプロジェクトの方が多いようです。
この問題の解決方法のひとつは、プロジェクトのメンバー全員でこの記事を読むことです!(真剣)
そしてもうひとつ、プロジェクトのメンバー全員でテストコードのコードレビューを実施してみてください。
たとえば、弊社ソニックガーデンではランチタイムにみんなでご飯を食べながらコードレビューをしたりします。
カジュアルな雰囲気でテストコードの善し悪しについて全員で議論すると、全員の意識を統一するのに非常に効果的です。
(もちろんアプリ側のコードについて議論するのもOKです)
参考: ソニックガーデンで行われているコードレビューの具体例をお見せします (SonicGardn Study #11 の補足として) #sg_study - give IT a try
テストコードなしでコードレビューをやっても意味がない
(2018.6.14 追記)
前述のアンケート結果を見た方はお気づきかもしれませんが、実は「テストコードがありません」という回答が55%で最も多くなっています。
うーん、テストコードをちゃんと書いてるプロジェクトって、あんまり多くないんですね・・・。
その一方で、「コードレビューならやってるよ」というプロジェクトは結構多いかもしれません。
もしみなさんのプロジェクトが「テストコードはないけど、アプリ側のコードはレビューしてるよ」というプロジェクトだった場合はちょっと要注意です!
そんなときは次の格言を額に入れて社内に飾っておきましょう。
「テストコードがない状態だとリファクタリングできないし、リファクタリングできない状況でコードレビューしても無駄なので、まずはテストコードを書いて下さい」
これは、とある勉強会で弊社ソニックガーデンの西見(@mah_lab)が「コードレビューははじめたけど、テストコードはまだなんです」という質問(相談?)に対して発したコメントです。
(一説によると、書籍「リファクタリング」でも同じようなことが書いてあるとか)
「テストコードがない → リファクタリングできない(振る舞いが変わっていないことを保証できない) → コードレビューしても意味がない」というのは本当にその通りだと思います(人間の手と目で毎回徹底的にテストする場合は別として)。
「きれいなコードを書くなら、まずテストコードから」ですね!
ペアプロのススメ
(2018.6.14 追記)
もしチームメンバーにテストコードを書くのが得意な人がいるなら、テストが苦手な人は積極的にペアプロ(ペアプログラミング)してもらいましょう。
テストコードの書き方を熟知している人から直接手ほどきを受けるのは、非常に効率の良い学習方法です。
実例として、ソニックガーデンの社内ではこんなやりとりが行われていたりします ↓
まとめ
というわけで、この記事では僕が普段意識している「テストコードの方針」をあれこれまとめてみました。
何をテストしたらいいの?どんなテストを書いたらいいの?と迷っている初心者プログラマさんは、ぜひ参考にしてみてください。
あわせて読みたい
- サヨナラBetter Specs!? 雑で気楽なRSpecのススメ - Qiita
- 【アンチパターン】Arrange、Act、Assert(AAA)を意識できていないRSpecのコード例とその対処法 - Qiita
- テストコードの期待値はDRYを捨ててベタ書きする ~テストコードの重要な役割とは?~ - Qiita
- コードレビューの極意。それは「自分のことは棚に上げる」こと!! - Qiita
ややRails寄りですが、過去に開催したこちらの勉強会の内容も参考になるかもしれません。