自動テスト内の分類基準は明解ではない

和田卓人氏:次に、テストサイズという考え方にいきます。自動テストにも「〇〇テスト」というやつがいろいろあるんですよね。

特に我々ソフトウェアエンジニアにとって馴染み深い名前はユニットテストとか、単体テストとか、インテグレーションテストとか、システムテストとか、エンドツーエンドテストとか。「〇〇テスト」というやつがいろいろあります。それらの分類基準は、実は言うほど明解ではないんですね。

ということで、ちょっと講演で時間差があるのでリアルタイムに決を採ることはしないんですが、ここでみなさんに質問です。ユニットテストとは何か。これが人によっていろいろ違ったりするんですよね。

チャットでもバンバン答えてもらえればと思うんですけど。ちなみに、データベースにアクセスするのはユニットテストですか? あなたはどう思いますか?

データベースにアクセスするのはユニットテストである、Yes/No。ネットワークにアクセスするのは? Yes/No。ファイルにアクセスするのはユニットテストでしょうか、Yes/No。じゃあ現在時刻にアクセスするのはユニットテストでしょうか、Yes/No。依存先のクラスに本物を使うのはユニットテストでしょうか。

あるクラスAのインスタンスのテストをしているとします。AがBとかCのクラスのインスタンスに依存しているとします。BとかCに本物を使うテスト、あるいはモックオブジェクトとかで置き換えたテスト。ユニットテストはどちらでしょうかという(質問をする)のも変な話ですが、本物を使うのもユニットテストですか?

今Zoomのチャット欄がバーッと流れています。「Yes」とか「No」とかがいっぱいありますね。そうなんですよ。なにが言いたいかというと、人によって解釈がブレブレなんです。「ユニットテストと単体テストというものは同じものを指しますか?」というと、人によって違ったりするんですよね。僕は技術書籍の翻訳とかもやるんですが、「Unit test」を「単体テスト」と訳すのは避けるようにしているんです。“ユニットテスト”と訳しているんです。

なんでかというと、企業によってユニットテストと単体テストは違うものを指していたりするからなんですよね。というぐらい、実はブレブレなんです。

自動テストの分野において大きく分かれるところの分水嶺の1つが、先ほどの5番目の質問、「依存先のクラスに本物を使う? 使わない?」というところで。実はスタイルが大きく分かれます。5問質問した最後のやつですね。

(スライドを示して)真ん中の点線で囲まれているところがテスト対象のクラスだとします。テスト対象のクラスが、別のクラスのインスタンスに依存しています。何かが横に2つありますね。

「本物を使うのもユニットテストだよ」と言う人もいれば、それはユニットテストじゃなくて、この四角の点線で置き換えているように「テスト対象を独立してテストさせるためにはモックとかスタブとかで置き換えるべきである」というような考え方の人もいます。

実は歴史的にはテスト駆動開発には2つの流派があって。デトロイト学派とロンドン学派というんですけど。デトロイト学派は概ね左側、「メモリ内であれば本物を使ってよし」という考え方をする。右側のSolitary Tests、「テスト対象を独立させてコラボレータ、モックやスタブにしよう」というような考え方がロンドン学派と言われたりします。ということで、ブレブレなんですよ。

ここまでブレブレだと、例えば「ユニットテストの方針はこうする」とかの議論がうまくいきそうでいかないんですよね。ここまでブレブレだと(うまく)いかないんですよ。だから例えば転職したりすると、(その企業で)「〇〇テスト」(という言葉)が指しているものがぜんぜん違ったりしていて。

一種のコントみたいに指しているものが違うみたいな感じになってしまいがちなので、「もっと一貫性のある分類ってなにかないかな」「曖昧さの少ない分類ってないかな」と考えるわけですよね。

より曖昧さの少ない分類「テストサイズ」

そういった時に、これはGoogleから始まったんですが……。具体的には『Googleのソフトウェアエンジニアリング』という本の中で説明されているんですけど。

例えばテストのユニットとか、インテグレーションとか、E2Eテストとかをよく「テストスコープ」と呼んだりするんですが、それに対してスコープじゃなくてテストサイズという考え方で分類する。分類するというより、「そういう分類の軸を設けたほうが曖昧さが少ないんじゃないか」というような考え方が出てきています。

そのことを「テストサイズ」と言います。「サイズ」と言っているから小・中・大とか、スモール・ミディアム・ラージとかがあって。

スモールテストは何かというと、1つのプロセスの中で閉じているテストです。だからプロセス外にアクセスしたらスモールテストではない。例えばデータベースにアクセスしたらスモールテストではない。ネットワークにアクセスしたらスモールテストではない、みたいなかたちです。

ミディアムテストは何かというと、1つのマシンの中に閉じているテストです。1つのマシンの中に閉じているというのは、実はけっこう応用が利いて、最近はDockerのコンテナとかで開発する時代になってきていますよね。Dockerのコンテナを組み合わせれば、1つのマシンの中でデータベースを立ち上げて、ブラウザを立ち上げてテストすることもできるわけですよね。なので、ミディアムというのはけっこう応用が利きます。ただ、マシンの中で閉じている。これがミディアムサイズのテスト。

ラージテストというのは、それらの制約がない。プロセスに閉じていないし、マシンにも閉じていないテストその他全部みたいなテストがラージテストという。

曖昧さがえらく少ない分類でやっています。ちなみにGoogleは超大企業なので、ラージの上のエナーマスという超巨大みたいなやつがあるらしいですが、一般の我々にとっては3つのサイズがあれば十分です。

なので、例えばGoogleのAndroidチームの開発においては、スモールテストはネットワークアクセスをしない、データベースアクセスをしない、ファイルシステムもアクセスしないみたいな感じで、(表に)No、No、No……その代わり超高速で動く。

ミディアムテストはどういうテストかというと、ネットワークはlocalhost。マシンに閉じていればアクセスOK、データベースはマシン内で立ち上げればOKみたいな感じなんですよね。

ラージテストはなんでもあり。全部本物にアクセスする代わりにすごく不安定で、すごく遅くなるという感じです。Googleはその外部システムがものすごくいっぱいあるので、たぶん数のレベルで超巨大(なテストサイズ)というものを設けているんでしょうね。

テストスコープとテストサイズの掛け合わせでわかる推奨されるもの

というと、我々が一般に理解しているユニットテスト、インテグレーションテスト、E2Eテスト。「仮にこの3つのスコープを設けるとしましょう」みたいなもの。それとテストサイズ。そのテストの実行が単一プロセスに閉じているのか、単一マシンに閉じているのか、それとも閉じていないのかは、実は3×3の表にできるわけですよね。そうすると、我々が目指したいところとか、大いに推奨するところも見えてくるわけです。

例えばユニットテスト。テストスコープのほうはちょっとフワッとした定義です。テスト範囲が狭い。エンドツーエンドテストは大外から全部をテストするみたいな感じなんですね。これとテストサイズのかけ算にすると、推奨エリアとか避けたいエリアが見えてくるわけです。

単一プロセスに閉じたユニットテスト×スモールテストはいくらでもスケールするので、一番推奨するエリアです。(スライドを示して)よく見ると真ん中に普通のエリアがあるんですね。インテグレーションが単一マシンに閉じた状態でミディアムテストとして動かせるというのは普通のことです。インテグレーションが単一プロセスに収まるものも、小さいシステムならあり得ますよね。書けるならコスパが良いです。

逆にインテグレーションテストが1つのマシンに溢れてしまうのは、できれば避けたい。例えばE2E×ラージ、E2Eテストがラージテストになるのは普通である。ただ、いっぱいあればあるほどどんどん開発が不安定になる。

エンドツーエンドテストとかラージテストというものは、当たり前ですが外部のネットワークにアクセスします。テストの中でflakyさ、不安定さの原因として一番大きいのは、外部ネットワークアクセスなんですよね。ということで、E2E×ラージは本質的にすごく不安定なんですよ。だけど、そこじゃないとテストできないものもありますよね。

それがエンドユーザーのレベルで対象のシステムをテストできるか、使えるかどうかというようなテスト。これはE2Eテストでしかできません。なので、普通かつCUJ(Critical User Journey)に絞りたい。CUJは「クリティカルユーザージャーニー」と言います。日本語で雑に訳すと、ド正常系ということですね。主要動線というやつです。

システムの主要動線に関して、つまり深さ優先のシナリオテストは、エンドツーエンドテストのラージテストじゃないとできないし、意味がないかもしれない。でも、実は「エンドツーエンドテストだけどSeleniumのコンテナとデータベースのコンテナがあれば走るよ」というシステムももちろんあります。

そうしたら、小さいシステムならミディアムでエンドツーエンドテストを動かすほうがコスパが良い。スモールなエンドツーエンドテストは原理上不可能に近いと思いますが、小さいシステムならある程度はある、みたいな。

逆にバッドエリアにいきましょう。ユニットテストなんだけどデータベースにアクセスしているテスト。けっこういっぱいあるんですよね。避けたいんだけど、仕組み上、仕方がない時もあります。

例えばRuby on Railsのモデルのテストとかはユニットテストと言っていますが、データベースがないと動かないものが多いので、実質的にはミディアムテストです。避けたいけど仕組み上、仕方ないというのもありますよね。だからけっこう遅くなってしまう。不安定ではないけど遅くなってしまう。

(スライドを示して)問題は一番右上のエリア。ユニットテストレベルとか、例えば画面のバリデーションのテストとかをラージテストレベルで行ってしまう。外部システムにアクセスし得るようなステージングとか、本番システムとかでやってしまうということもけっこうあるんですよね。

ということで、そういったところを避けたいんですが、例えば画面レベルのテストでエンドツーエンドテストと言っているにもかかわらず、やっているのは網羅的に画面のバリデーションのテストとかだと、実質的にはラージレベルでユニットテストをやっていることになっちゃうんですよね。

こういうのを避けようということで、実はものすごく単純化すると、コスパの良いエリアと悪いエリアがあります。(そうなると)「なるべく投資対効果の高いようにテストを設計していきたいですね」という話になるわけです。

ちなみに「DBアクセスするユニットテストがダメ」と言っているわけではありません。仕組み上避けがたい時もあるし、それによって担保できるものもあります。

(次回につづく)