この記事は株式会社ビットキー Advent Calendar 2022 24日目の記事です。
ソフトウェアエンジニアの @tshiraki が担当します。
はじめに
おはようございます。みなさんテストコード書いてますか。
最近、ふと自分がなんのためにテストを書いているのか考えることがありました。それについて、この機会に色々と書いてみようと思います。ついでに最後のほうで、テストで工夫していることを少しだけ紹介したいと思います。
なぜテストコードを書くのか
テストに対する向き合い方は、自社サービス、OSS、受託開発など、対象や立場によって大きく変わるので、ここでは自社サービスのテストコードを書くことを考えることにします。そしてテストコードと言ったときにまず単体テストを思い浮かべることが多いと思うので、まずは単体テストについて考えてみます。
そもそもサービスを提供するためには、テストをする必要があります。当たり前ですね。そして単体テストだけでは、一般に品質を保証することはできません。品質保証に踏み込むとテーマが変わってしまうためこの投稿では大きく触れませんが、テストコードを書いても書かなくても結局テストをする必要があるということです。
では、なぜ書くのか。
これを考えるには、書かなかったらどうなるかを想像しながら考えると分かりやすいです。
未来の自分とチームメンバーのため
そもそも自社サービスの開発は、一度作ってリリースしたものに対して継続的に変更することが多いです。そしてその変更は、未来の自分やチームメンバーがすることになります。そのときに、過去に作られたコード、つまり自分が書いたコードにテストがないと、変更の危険度が跳ね上がります。
結果、リリース直前に問題が発覚して徹夜するとか、リリース後に問題が発生して計画を変更して対応して、遅れを取り戻すために徹夜するとか、大事な何かを諦めるとか、そういうことに繋がります。つまり自分がテストを書かないことによって、未来の自分やチームメンバーの身に起きるリスクが上がる、ということです。
テストを書くと、そういったリスクを少しでも減らすことができて、自分だけでなく周囲も守ることに繋がります。
サービスの成長速度を上げるため
これについてはOSSのIssue解決のシーンから想像すると分かりやすいかもしれません。そこでは、テストコードは破壊から身を守るために存在しているやつ、というだけではなく、解決のための重要なヒントになりえます。
そもそも、そのリポジトリを深く理解をしているとは言えない人たちが貢献してくれようとしている場面で、テストがないと原因を特定しにいくことすら難しかったりします。つまりOSSの場合、テストがないとリポジトリが成長しづらかったり、場合によってはそのまま成長を止めたりします。
これをそのまま自社サービスにそのまま置き換えてみると、テストがあれば、ジョインしたばかりでドメイン知識がまだ浅い人が早く戦力になりやすいということです。
今の自分のため
テストの対象や粒度、やり方や習熟度などにもよりますが、そもそもテストを書いたほうが開発が早く完了することが多いです。テストを書くコストとのトレードオフの話を読んだり聞いたりすることがありますが、それは何か大事なことを忘れている気がします。
冒頭で書いたように、テストコードは書いても書かなくても、テストはします。そのときに様々なケースに対して手でぽちぽちしたりcURLで実行しながら動作を確認して、必要があれば修正して、という手順を繰り返すより、テストコードを用意して通してから最後に動かして確認するほうが結局早かったりします。
書くと工数がかかるから書かないというのは極端で、そうではなく、少なくとも書かずにぽちぽちするより効率よく検証できるのであれば、書くべきだと思います。
結合テストやE2Eテストについて
前述したように、単体テストでは一連の動作は保証できません。もっと大きな粒度である結合テストやE2Eテストであれば、単体テストで確認したモジュールを組み合わせた動作を検証することができます。それにも関わらず、結合テスト以上のテストコードを記述する話に触れる機会は、単体テストに比べてとても少ないです。
これはシンプルに、単体テストのほうが効率がいいからだと思っています。書くのが大変なだけでなく、実行に時間がかかって開発体験としては微妙だったり、環境維持や実行にお金がかかるということもあります。
逆に言えば、それらの問題が解消するのであれば、手を出す価値があると思います。これについて、最近取り組んでいるやり方を紹介したいと思います。
結合テスト
結合テストというと粒度が色々あるので、ここではクリーンアーキテクチャでいう、Usecase (Interactor) をテストすることを考えてみます。その層では、実行から結果を得るまでの過程に、複数の処理や永続層とのやり取りなどが含まれます。
そのときに、多くの処理をモックにしてしまうと、結局Interactor単体の振る舞いだけを検証することになり、その中の処理を含めた結合テストとは言えなくなります。
そのため、単体テスト感覚でテストは記述するものの、極力モックに頼らない、ということをしました。データベース、ストレージ、Pub/Sub、検索エンジンなど、可能な限り本物を使うように、ローカルでコンテナを起動し、そこに繋げて実行する、という形です。
すべてローカルなのでお金はかかりませんし、テスト実行前にコンテナを起動しておくだけで、単体テストと同様に気軽に何度も実行できます。複数回の実行や、並行して実行される他のテストの影響を受けづらくするため、テスト実行ごとにデータベースのスキーマを作る工夫などもしています。
E2Eテスト
上記のようにモックに頼らずにテストを実行することができる環境を一度整えると、外部から呼び出す場合のテストも簡単に書けます。この場合、例えばiOSアプリからバックエンドのAPIを呼ぶケースの場合、Swiftのテストコードで外から見たテストを書いておけば、それをそのままAPI仕様書として渡せたりもします。
とはいえ記述効率は単体テストに比べると当然落ちるので、できるからといってこれだけに頼らず、単体テストを基本とした上での追加の選択肢というバランスがいいかなと思います。
最近はそんな感じで、様々なシーンにおけるベストを日々探っている状況です。
まとめ
テストは書いて当然という人もいれば、あまり書けていない人や普段は意識していない人もいるかと思います。
今回は自分がなんで書くのかを考えてみて、自分や身近な人のためという理解をしてみたらしっくりきたので、こんな内容を書きました。誰かのためって考えると前向きになったり丁寧な仕事ができたりすることってありますよね。
成長速度とかの話は、テストコードを記述するコストを誰かに説明するときのヒントにでもなればうれしいです。
それでは良いクリスマスイヴを。
お読みいただきありがとうございました。
明日は株式会社ビットキーAdvent Calendar 2022 最終日です。VPoEの山本 @kanhigh@github の投稿です。
お楽しみに!