テスト用に使う偽物「テストダブル」

和田卓人氏:じゃあ次。テストダブルの話にいきます。「忠実性と決定性のトレードオフを理解しよう」という点です。これはもうちょっとあとにまた出てきます。

テストダブルというもので、モックオブジェクトとかスタブとかを使って、本物ではない偽物をテスト用に使ってテストをすることはよくありますよね。

データベースの偽物とか外部システムの偽物とか、AmazonのS3の偽物とか。本物を使えない、あるいは使いたくないからテスト用の偽物で置き換えるということをよくやります。

こういう、テスト用に使う偽物のことを総称して「テストダブル」と呼びます。テスト用の偽物を何と呼ぶかは混乱しきっていた歴史があります。ある人はこういうテスト用の偽物のことを「モック」と呼んで、ある人は「フェイク」と呼んで、ある人は別の呼び方をしてみたいな感じになっちゃうんですね。ある人が「スタブ」と言っているものを他の人は別の言葉で指しているとか、そういうのがいっぱいあるんですよね。

混乱しきっていたところを1つの本が大統一しました。それが『xUnit Test Patterns』という本です。ものすごく分厚い本なんですよね。(スライドを示して)この本が『xUnit Test Patterns』です。

ジェラルド・メーサーロシュという著者の方が、その混乱しきっているテスト用の偽物のことを整理整頓しました。テストに使う偽物のことを総称して「テストダブル」と呼びます、と。

テストダブルの詳細は、ダミーとかスタブとか、スパイとかモックとか、フェイクとかになります。それぞれがどういう意味なのかとか、どういう役割は時間の関係で深入りできないんですが、我々がよく「モックに置き換えよう」と言っている時、そのモックは狭い意味でのモックと、広い意味でのモックがあると思ってください。

広い意味でのモック、テスト用の偽物のことを総称して、我々はよく「モック」と呼びがちですが、より細かくより正確にはテストダブルのことです。

つまり、広義で総称したテストダブルのことをモックと呼んだり、あるいはテストダブルのサブ概念のことをモックと呼んだりしているので、ある人が「モックオブジェクト」と言った際にどちらを指しているのかは、まだ混乱しているような感じだったりするんですね。

テストダブルの利点でまず1つ間違いなく良いのは、テストしにくいものをテスト可能にすることです。例えばネットワークエラー。本物のネットワークでネットワークエラーを起こしてテストするのは不可能ではないんですが、だいぶ面倒くさいところがある。ディスクフルの場合の振る舞いとか、そういった例外系のテストにテストダブルはすごく強みを発揮します。

あとテストダブルは本物よりも速度が速い。メモリ上でエミュレーションしたりするので速度が速いことが多く、かつ不安定さも減ります。決定性とは毎回同じように動くということです。なので、速度と決定性が上がります。

その代わりモック、テストダブルにはデメリットもあります。まず1つはテストが脆くなることですね。モックオブジェクトとかテストダブルを使い過ぎると、実装の内部に深く根差したテストを書いてしまいがちです。

そうすると外部から見た振る舞いは変わっていないにもかかわらず、ささいなところですぐに失敗するようなテスト。細か過ぎる、詳し過ぎるテストになってしまいがちです。これが脆いテストを招いてしまう、偽陽性を招いてしまう。

もう1つはテストの偽陰性を招くということもありますね。よくよく見てみると、自作自演になっちゃっているテストってすごくあるんですよ。

データベースを偽物に置き換えているとします。データベースを偽物に置き換えた上でデータベースアクセスするテストを書いていると、「実はデータベースにアクセスしたらこうなっていること」みたいなところが周り回って、期待値とかも全部モックのほうで用意していて。

「自分がこう動くべき」と思った振る舞いをモックにさせて、そのとおりに動いているようなテストになっちゃっていて実はなにもテストになっていないというのもけっこうあるので、これもすごく注意してください。

信頼できるテストのための「テストピラミッド」

次にテストピラミッドです。今日の講演のテーマもそうですが、私の講演のテーマは自動テストをどれだけ中長期的に信頼できる状態にしていくかということです。

なので、信頼できるテスト、(つまり)成功も失敗も信頼できる。成功していればリリースできるし、失敗していればコードを直さなきゃいけない、かつ、なるべく早く信頼できる結果に到達するようなテスト群を作りたいんですね。

ということで、そういったテストのモデルは、よく「テストピラミッド」と呼ばれています。どのくらいのところにどのくらいテストを書いたほうがいいのか。テストケースの数の理想的な比率を一種のピラミッド型で面積を示した絵みたいなものなんですね。テストピラミッドでは、ユニットテストが全体を分厚く支えているべきであるという考え方があります。

なぜならユニットテストは実行のコストとか記述のコストが低くて速度が非常に速い。それで同じように毎回動いてくれる。そのユニットテストの上にインテグレーションテストが乗っていて、その上にエンドツーエンドテストが乗っている。エンドツーエンドテストは実行速度が遅く決定性が低く、動作が不安定、それでコストが高い。その代わり忠実性が高い。忠実性というのは本物らしさ(のこと)ですね。

本番環境の本物の動きを模している度合い自体は、エンドツーエンドテストはテストに本物を使うので忠実性が高いです。ユニットテストはテストに偽物、テストダブルとかを使うことが多いので、忠実性は下がっていく。つまり自作自演のリスクをはらんでいるということになるわけですね。

テストピラミッドと対照的なアイスクリームコーンアンチパターンがあって、理想的なのはテストピラミッド型ですが、現場でよくあるのがアイスクリームコーン型。つまり、手と目でたくさんテストをしていて、エンドツーエンドテストとかGUIレベルでのテストが多くてユニットテストはぜんぜん少ないという、アイスクリームコーン型のアンチパターンに陥ってしまう現場がすごく多いです。

最近は議論もいろいろあって、「ピラミッド型は古いんじゃないか」とか「トロフィーモデルが良い」とか「ハニカムモデルが良い」とか、いろいろ言われるんですけれど。でもこの議論のブレをよくよく見てみると、ユニットテストとは何かとか、インテグレーションテストとは何かというところの、先ほど言っていた解釈のブレがそのままモデルの議論のブレにつながっちゃっていることが多い。

実は一貫した分類基準に照らしてこれらのモデルを再び見てみると、ピラミッド型であることが多かったりするんですよね。

(そうなると)「その一貫した基準って何だっけ? ブレがないテストの分類基準ってそういえばさっき話してきたよね」という話になるんですよね。テストピラミッドはよくE2E(End to End)、インテグレーション、ユニットテストといったテストスコープで各段が定義されることが多いんですが、我々はよりブレの少ない一貫した分類基準を議論してきました。

つまり、テストサイズでピラミッドを組むほうがよりブレの少ない議論ができて、テストをどういった比率で運用していくかというところを、よりブレのないかたちで議論できるようになるんですね。ユニットテストが全体を支えているべきであるというより、スモールテストが全体を支える。

スモールテストの中にはユニットじゃないやつもあったりしますよね。インテグレーションテストでもスモールテストで動かせるものもあったりする。そういう比率の中で、スモールであればあるほど速度が速く安定しています。ラージであればあるほど速度は遅くて不安定なんだけど、その代わり本番を模しているというトレードオフのモデルができます。

自動テストのサイズダウン戦略

さあ、最後です。でも多くの現場ではアイスクリームコーンから始まります。なぜなら自動テストが書けていないとか、書きにくいところからだんだん……。今は手動テストでカバーしていたところを、例えばAutifyを使ったりエンドツーエンドテストを自分で書いたりして、自動テストがそもそも書きにくいところに対して外から包んでだんだん自動化の度合いを高めていくことをするので、最初のテストの比率としてはアイスクリームコーン型から始まる。

特にレガシーシステムとか、大規模システムとかを抱えていると、アイスクリームコーン型の比率から始まることはとてもたくさんあります。最初はだんだん外から自動化していくのは悪いことではありません。

ということで、最後は自動テストのサイズダウン戦略ということで、サッと話します。

ただ、アイスクリームコーンのかたちだと長続きしないんですよ。ラージテストが多過ぎると実行結果が不安定になっちゃって、偽陽性が増えてしまって信頼性が損なわれてしまう。結果的にテストの失敗に対してそれが日常になって鈍感になっちゃう。だからアイスクリームコーンをどうやってなるべく早いうちにピラミッドにしていくかが大事なんですね。

ということで、まずラージテストをミディアムテストにどうやって移植していくかと、これはテストダブルの仲間の中で一番地味ですが、フェイクオブジェクトというものを使います。フェイクオブジェクトは何かというと、テスト用の代替実装のことです。

具体例を挙げるとわかりやすくて。例えばAmazonのDynamoDBを使っているとします。DynamoDB Localという、AmazonのAWSが推奨しているテスト用のエミュレータがあったりします。このDynamoDB LocalはDockerコンテナ上で動いたりするんですね。本物のDynamoDBを使ってアクセスするのは、外部ネットワークにアクセスしにいくから、当然ラージテストです。なんですが、DynamoDB LocalをDockerコンテナで立ち上げてCIのマシン上で動かせば、サイズはミディアムサイズに収まりますよね。

つまり、DynamoDBを使った本番のコードであっても、ラージテストしかできなかったものが、フェイクオブジェクトを使えば、つまりテスト用の別実装を使えば……。例えばDynamoDBに対するフェイクオブジェクトがDynamoDB Localです。この実装を使えばミディアムサイズに収まるとか。LocalStackというOSSのフェイクの集合もあったりするので、この実装を使えばラージからミディアムに持っていける。

じゃあミディアムからスモールにどうやっていくかというと、これはテストをしたいところとテストがしにくいところを分離することによって分けることができます。テストしたいところがスモールテストしにくいとか、そもそもテストしにくいこともよくあるんですよね。

(スライドを示して)そういった時に、例えばこの歯車みたいなやつがテストしたいロジックだと思ってください。テストしたいロジックがあるけれど、それがテストしにくい入れ物、例えば外部環境上で動いてしまっているとか、あるいはUIに密結合しちゃっている。だからサイズが上がらざるを得ないみたいなものもよくあったりするんですよね。

これの細かい説明はさすがにもうする時間はないんですが、テストがしにくい要素を細かく薄く切り取って、テストしたいところを中で抽出して、例えば関数に抽出したりクラスに抽出したりすると、インメモリでテストが書けるようになりますよね。スモールテストが書けるようになる。

この緑の丸ポチが付いたテストしにくいところは、ミディアムテストとかラージテストをせざるを得ないんだけど、テストしたい大部分はスモールテストが書けるかたちでできます。

そうすることによって、アイスクリームコーン型からピラミッドに移植ができるわけです。

テスト全体の信頼性を維持するために

ということで、これまでの知見を1枚絵にまとめてみると、こうなります。テスト全体の信頼性を維持するためにはどうするかというと、ブレがない基準でピラミッドを作りましょう。各テストサイズでピラミッド型を配置します。サイズダウンはどうやるかというと、テストダブルでします。ラージからミディアムへテストダブルでサイズダウンします。

ミディアムからスモールへはテストダブルを使ってもいいけれど、先ほどのHumble Objectパターンみたいなテストが容易になるような設計とか、あるいはドメイン層の抽出とか、リファクタリングとか、そういったテクニックを使ってテストしたいところをスモールにどんどん切り出していくかたちになります。これがテスト全体の信頼性を維持するモデルです。

和田氏のおすすめ本と、お知らせ

ごめんなさい。すごく時間がオーバーしている。これで終わりです。

最近良い本が出過ぎて、私の仕事を脅かすようなライバルが出てきています。例えば『単体テストの考え方/使い方』という本があります。この本は内容が良過ぎて、私がいろいろ説明したいことがほとんどこの本に書いてあるような事態になっています。とても良い本なのでおすすめです。私の活躍できる領域を明らかに蝕んできているぐらい、良い本です。

もう1つ、いかにも私の言いそうなことを言うAIが出てきました。「実行順序に依存するユニットテストについて、t_wadaさんなら何と言うでしょうか?」というと、いかにも私が言いそうなことをサラッと答えるようなAIが出てきているんですよね。実際にこれは私の意見と一致します。エキスパートに聞かなくても聞けるような時代がやってきているかもしれない。エキスパートにとっては脅威を感じるような時代にもなってきておもしろいですね。これはおもしろい時代です。

最後に悲しいお知らせがあります。このようなコラムを連載してきた『WEB+DB PRESS』という雑誌ですが、残念ながら休刊になることが決まりまして、次回(vol.136)が最終回です。最終回をこれから書くところです。

いったんきちんとしたかたちで締めようと思うので、ぜひ読んでみてください。

ということで私の講演は以上です。ご清聴ありがとうございました。