こちらの感想
モチベーション
単体テストの使い方を読んでから完全に古典学派寄りの思想でテストを書いている。しかし、世間ではモックを使ってテストを書いている方々も多くそれを否定するつもりもないし自分もモックを使う時は使う。
ある日、社内のレビューでモックを使ってテストを書いていたらテストすることがなくなったというようなコメントを見かけた。
「なんでや、そんなわけあるかい」とか一瞬思ったけどこれには経験がある。テストするために複雑な依存関係を全てモック化したところモック化しただけで終わってしまった。
アサーションしようにもモック(正確にはたぶんスタブ)で設定した振る舞いは自分が設定したもので、それが順々に渡っていくだけなのでなんだかテストしてる気がしない。このような感覚を自作自演とどこかで言ってたような気がする。
古典学派的なテストが基本的にはいいのはわかる。しかし、現実問題よくない設計のまま突き進まなければならないときも多い。さらに昔よりも現代の開発は複雑で外部のSDKやAPI、マイクロサービスなど全てを動かしてテストするのが難しくなってるんじゃないかという気もしてる。
これは盲目的に単体テストの使い方に従って古典学派を推す前にロンドン学派のテストも学ぶべきだなと思った。
調べたところt-wadaさんが古典学派のバイブルがt-wadaさんが翻訳しているテスト駆動開発でロンドン学派のバイブルが赤い表紙の実践テスト駆動開発だということを言っているポストを見かけたので読んでみることにしたのがモチベーション。
E2Eでテストする
この本の大半は本のタイトル通りテスト駆動開発(TDD)について書かれているが最初に書くべきテストはE2Eテストであるとされている。
テスト駆動開発はテストファーストであることは広く知られていると思うが最初に書かれるべきテストの種類についてはあまり述べられていなかったと思う。
E2Eを最初の受け入れテストとして書く理由はシステムの振る舞いをテストするには外部環境との相互作用も含んでいるからだ。これは内部のコードを直接呼び出すようなテストではテストできない。
最初にE2Eテストを書く場合、最低限動作するプログラムの実装が必要だ。
これを書籍内では動くスケルトンと呼んでいる。
このようなスケルトンから作ること、そのためにE2Eテストから書くことはシステムが最低限動く状態を作るという意味で開発の初期に作ることで価値を発揮する。
書籍内で書かれているE2Eテストにはコードのpushをトリガーに自動テスト、ビルド、デプロイまでが完了することを含んでいる。つまり、この受け入れテストが通ればデプロイできることを意味する。
こうしたデプロイ作業などは最もエラーが起こりやすい部分で手動ではなく自動で実行したい。
E2Eテストを最初に作ることはこのようなデプロイまでのフローを自動化するための試行錯誤を早めにやることにつながると書かれている。
また、デプロイに必要な作業はもっと多くあるとされている。例えば、DBのマイグレーションに承認フローなどがあり6週間もかかるのであればリリースの2週間にそのことを知るのでは遅すぎる。
このような、作業フローを早めに知るという意味でもE2Eテストから書くことが重要とされているようだ。
単体テストの使い方ではシステムの振る舞いをテストしろと強く主張されていたように思う。そして、モックを使ったテストでは全ての関数をテストすることになるとも書かれていたような気がする。
しかし、この書籍でも関数ではなく振る舞いをテストしろと書かれている。
E2Eテストを書くことは外部システムからの相互作用をテストすることになり、それは最も難しい。
難しいことだが最も重要な部分でもあるため最初に苦労することになってもE2Eテストを最初に書くとしている。
動くスケルトン
動くスケルトンについてもう少し触れておきたい。
E2Eテストを最初に書く難しさはアプリケーションを動かすインフラ環境やデプロイまでのフローなどアプリケーション以外の要因も絡んでくるからだ。とにかくやることが多すぎる。
なのでこの問題は2つに分けようねとしている。一つは動くスケルトン。もう一つがアプリケーションの機能単位で広く知られているテスト駆動開発のサイクルを回すこととしている。
動くスケルトンでデプロイまでのフローをテストし、デプロイできる状態になってから、機能単位でTDDのサイクルを回していこうみたいな感じだ。
具体的に動くスケルトンは可能な限り薄い実装とされていて、DBの初期化だったりHTTPサーバーを動かすみたいなものを指しているよう。
この動くスケルトンの話は本当に0からシステムを開発するときの話なのかなと思う。確かにビルドやデプロイ、自動テストの仕組みを後回しにしてリリース前に慌てるというのはとても経験がある。
というかアジャイル開発しようねみたいな話なのではないだろうか。
小さく動くものを作るとかデプロイサイクルを多くするとか。
デプロイフローが完全に自動化されていて安定したものがある中で開発できるのはすごい安心感があって開発スピードも上がるよなと思う。羨ましい
そのような安定した開発環境下でTDDのサイクルをまわして機能開発をしていこうねというのが動くスケルトンの話なのかなと理解した。
テストが書けなかったらリファクタリングする
テストが書きづらいのは設計が良くないからというのは単体テストの使い方でも言われているし割とみんな認識していることだと思う。
単体テストの使い方ではテストを書くのが難しいのは設計が良くないからなのにモックを使ってそのことから目を逸らしているみたいなことが確か書かれていた。
しかし、この書籍ではちゃんと設計と生き続けるシステムと向き合っている。TDDのサイクルで失敗するテストが書きづらかったら設計に関する警告だとしてまずリファクタリングする。そして、テストが書ける状態にしてからTDDのサイクルに入るのだそうだ。
当たり前のことを言っているようにも聞こえるがテストが書きづらかったらまずリファクタリングするという心構えはすごくいいなと思った。
そう思って開発することで積極的にリファクタリングをすることになるし、常にテストが書きやすいかを意識してコーディングすることになるし、最低限の保守性を持ったシステムを初期から作ることができる気がする。
システムをとりあえず作って、溜まった負債を返すためにテストとかも書いて、設計も頑張ろうみたいなのでは遙かに遅い。
やっぱりテストと設計は切り離せない。良い設計でシステムを開発し続けるためにはテストを書き続けないといけないし、テストについて考え続けないといけない。
外部システムのモック
モックするのは自分の持っている型だけ。
サードパーティーのシステムなどを使う場合、自分たちの管理下にないシステムのためそのようなシステムと完全に一致するモックを作るのは難しい可能性があるということ。
そして、サードパーティーの外部システムをモックしようとした場合、大抵複雑性を持ち込むことになる。
といった理由でサードパーティーの外部システムは薄くラップしてモックしたほうがいいよという話。
カプセル化とモック
モックが欲しくなるケースとしてカプセル化のトレードオフという考え方がある。
オブジェクトのカプセル化を頑張ることでオブジェクトの情報を隠蔽できた代わりにオブジェクトの値を取り出してアサーションするのが難しくなるという話。
これはだいぶオブジェクト指向の話でもあるがそういった時にモックが必要になるとされている。
なるほどとも思ったけど具体的にどういうテストのことを言っているのかはあんまりイメージできなかった
カプセル化され情報の取得ができないオブジェクトのアサーションをモックを使うことで解決できるのだろうか?
持続可能なテスト駆動開発
本書ではいたるところでコードやテストの可読性の重要性について書かれている。おそらく、本書の本質のテーマが「持続可能なシステム開発」だからだと思う。これは単体テストの使い方でも散々言ってた気がするし他の設計本でも言われてた気がする。
長期的に動くシステムは機能追加、バグ修正、リファクタリングを繰り返し育てていく必要があり、作って終わりのシステムはほとんどない。
このことを念頭に置かず作られたシステムに人々は苦しまされてきたからこそ多くの人が設計について本気で取り組み様々な議論や概念を生み出してくれたのだと思う。
テストは振る舞いをテストすべきと書いたが一見してそのテストがどのような振る舞いをテストしているのかがわかることは重要。これがどのような振る舞いをテストしているのかがわからないと後世の開発者がテストの意図を理解するのに苦労し頭を抱えることになる。
持続可能なシステムには積極的なリファクタリングが不可欠でリファクタリングのハードルを下げるのはテストだ。そのテストが難解であると結局のところリファクタリングのハードルが上がり、システムの成長を妨げることになる。
そのため、本書では可読性や保守性の観点で以下のようなことが記載されている。
テストメソッドの命名
できる限りアサーションは少なくする
できる限りエクスペクテーションも少なくする
AAAもしくはGiven-When-Thenパターンを使う
わかりづらければカスタムのマッチャーを使う
モックを使ったテスト
本書では以下のような用語が登場する
アローアンス
エクスペクテーション
モック
スタブ
モックとスタブは同じ意味合いで使われてしまうことが多いように思うが、ちゃんとモックを使ったテストを書くのであればやはりちゃんと区別できるようにしておいたほうがいい。
モックもスタブもテストのために偽の振る舞いをさせるためのテストダブルに分類されるが、モックは内から外への振る舞いを置き換えたい時に使うものでスタブは外から内への振る舞いを置き換えたい時に使う。
テスト対象のコードに依存関係がある場合、それらを隣接オブジェクトと呼び、隣接オブジェクトをモックやスタブに置き換えてテストをするのがロンドン学派的なテスト。
このとき、全てのモックやスタブの振る舞いを確認する必要はない。それは過剰なチェックであり、テストの保守性を損なうことになる。
モックやスタブの振る舞いの確認をエクスペクテーションと呼び、特に振る舞いは気にしないモックやスタブはアローアンスと呼ぶ。
必要最低限のエクスペクテーションをモックを使ったテストでは書き、不要な確認は全てアローアンスとすることで可読性の高い、かつ振る舞いを確認できる良いテストが書ける。
この、エクスペクテーションとアローアンスを意識してモックを使ったテストを書いている人はあまりいない気がする。
逆に言えばエクスペクテーションとアローアンスを気にせずモックを使ったテストを書くからテストすることがなくなったという感覚や実装と密になりすぎた壊れやすいテストになってしまったりするんじゃないだろうか。
モックの歴史
最後にモックオブジェクトという概念が生まれるまでの歴史が書かれていたので軽くまとめておく。
始まりは1999年ごろまで遡る。場所はロンドンで何人かのソフトウェアアーキテクチャグループの議論から始まっているらしい。当時はテストを書きやすくするためにオブジェクトにgetterを用意しようという風潮があったらしくそれをよく思っていなかったそうだ。本書でも書かれていたがgetterを追加することでテストはしやすくなるがオブジェクト指向のカプセル化を壊すことになるというオブジェクト指向との対立みたいなものがあったようだ。
そこで、今でいうDI(依存性の注入)がもたらすコンポジションという概念を取り入れることでgetterを撲滅したそうだ。
そこから、オブジェクト間で何が起きることを期待しているかという話題になることが多く、注入されるオブジェクトの変数にはexpectedXXXのような名前が付けられることが多かったそう。
そして、そういったアイデアをクラス群として抽出したものがモックと名付けられたそうだ。
その後、数々の言語とライブラリに改良が加えられ公開されたことで今ではテストツールとしてモックは広く知られることになる。
本書でも書かれていたがモックの誕生はテストを早くしたいとかではなくgetterを無くしたいという設計からのアプローチが始まりだったそうです。
おわりに
単体テストの考え方ではロンドン学派のテストは全てモックを使い、全ての関数をテストするようなことが書かれていた気がする。(書かれてなかったらごめんなさい)
そのため、モックの使用に全振りしたテスト手法なのかと思っていたが少なくとも本書を読んだ感じそこまで古典学派との差はないように感じた。
ただ、アプローチというか解決したい問題が少し違うのかなと。
本書の著者はタイトル通りかなり実践的な目線でテストと向き合っており以下のような特徴があるように感じた。
最初に書くテストはE2Eの受け入れテストだとしている
これは開発の始まりからリリース、その後の運用まで考えた時にコード以外の問題も含めて多くある問題点を早めに洗い出すという目的やそのE2Eテストが通ればデプロイできるという状況を早めに作り出しておきたいという思想があるのかなと思う。
最初のE2Eテストを書いた後は古典学派と一緒でひたすらTDDのサイクルを回す
単体テストの考え方もそうだったがシステムの保守性や持続性を最も重要としており、可読性を重要視している
なので積極的にカスタムマッチャーの作成を推奨してる
テスト関数の命名も何をテストしているかがわかるため重要としている
アサーション、エクスペクテーションは最低限にする
古典学派同様、単体テストは振る舞いをテストすることを重要としている
モック、スタブを使ったテストでは何を検証し、何を検証しないかをちゃんと考えアローアンスとエクスペクテーションをちゃんと使うことが大事
もっとモックを使ったテストのノウハウや考え方みたいなのが書いてあると思っていたが実際はほぼほぼTDDとアジャイルの話で単体テストの考え方と類似している記載も多かった気がする。
この書籍での学びとしてはモックのアローアンスとエクスペクテーションの話で闇雲に全ての依存オブジェクト(書籍では隣接オブジェクトと言っていた)をモックまたはスタブにし全ての引数や戻り値を検証する必要はないということ。
振る舞いをテストするのに本当に必要なエクスペクテーションのみ作成し、それ以外はアローアンスとして無視して良いということだ。
この書籍を読むきっかけになった
「テスト対象の依存を全てモック化したらテストすることがなくなりました。」
というやつはきっとこのエクスペクテーションとアローアンスを理解しないまま、目的がテスト対象クラスを動かすためにモックオブジェクトを作成することになってしまったからなのではないかなと思う。
この書籍でも単体テストの考え方でも書かれていたがDBのテストは遅い。仕事ではGoのDBテストをtestcontainersでコンテナを起動してから実行するようにしているが2,30秒くらいかかる。
CIでマージ前に1回だけ動くだけならいいがローカルで開発しているときはテストの実行とコーディングのサイクルを高頻度で回すことになるのでDBのテストに数十秒かかるのはかなりストレス。
加えて、Dockerのような仮想コンテナを使う場合、コンテナ特有のセッティングも不具合も発生するので思ってるよりも導入コストがかかると最近思い出した。
単体テストの考え方を読んだあとはモックは極力使わない古典学派的なテストを推していたがこの書籍を読んでモックを使ったテストに寄せていこうかなというお気持ちになった。
タイトルにあるように実践的な観点で見た時、なんというかモックを使ったテストの方が都合がいいことの方が多い、ような気がする。というか、現代のシステムが複雑化していてモックなしにテストを動かすのがけっこう大変な気がしてる。
しかし、モックを使った単体テストを書く場合、テスト対象のオブジェクトの隣接オブジェクトを実際に動かし、全体を通した振る舞いをテストするインテグレーションテストもしくはE2Eテストが必須にもなってくる。
単体テストの考え方ではモックを使ったテストの最大の問題は偽陽性を持ち込むことだとも述べられていた。
実装レベルの話になるとモックを使った単体テストとインテグレーションテストをいいバランスで書いていくのが理想的な気がするが結局インテグレーションテスト書くならモック使った単体テスト必要なくないかという気もしてしまう。
どこまでをモックを使った単体テストとして書き、どこまでをインテグレーションとして書くかみたいな基準を自分の中で持っておかないとダメかもしれない。
正直、よくあるAPI開発とかであればDBはtestcontainersを使ったインテグレーションを書き、ドメインはなるべく副作用がないように抽出してテストを書き、Controller的なところは依存オブジェクトを全部モックにして全体的なつながりをテストするみたいな流れで最低限の実装者である自分の心理的安全性を保ち開発したいなというのが今のところの本音。
また、手のひら返しでモック使うのやめるとかになるかもしれないがとりあえずモックを積極的に使っていってもいいかなという気持ちになりました。
テストも設計もむずい