2023年5月17日から5月19日にかけて開催された Qiita Conference 2023 にて、弊社の Senior Technical Support Engineer である末村 拓也が『リファクタリングが先か、テストが先か – E2E自動テストの理想と現実』というタイトルで講演を行いました。本記事はこのセッションを元に、ブログ向けに若干アレンジを加えたものとなります。

概略

この記事では、以下のような内容について説明します。

  • 自動テストコードはアプリケーション本体のコードと 依存関係 を作る
  • 一般的に、 不要な依存関係 を排除するのが良い設計と言える
  • 一方で、E2Eテストは GUIに対して強い依存関係 を作る
  • テストの準備などで GUIとの不要な依存関係 を作らないようにするのが重要
  • 不要な依存関係を減らすために、テストレベル を一つ落とす(ユーザーストーリーE2E
  • 低いテストレベルほど、 テスタビリティ実装への依存度 が高まる ≒テストしやすい設計、テストのためのAPIが欲しくなる
  • 低いテストレベルの実装が困難な場合は、高いテストレベルから始めて、順番に低いテストレベルも実装していく

イントロダクション

バーでの会話

Two engineers are talking about challenges on E2E test.

この間、友達のソフトウェアエンジニアと一緒に飲みに出かけた時の話です。

エンジニアが二人でバーカウンターに座ると、たいてい技術的負債の話が話題になることが多いですね。その日の話題はE2Eテストでした。E2Eテストというのはこの後も説明しますが、本番環境に近い環境で実施する、ビジネスプロセスを最初から最後までユーザー目線でテストするタイプのテストのことですね。その友達は、E2E自動テストに関する次のような悩みを話してくれました。

「テストを実行したことによる副作用で、そのテスト自身も含む他のテストが動作しなくなる – 例えば、テストAを実行したことで、データベース内のデータが変更され、同じデータを参照する他のテストや、あるいはテストAの再実行でさえ失敗してしまう。

また、テストの前にテストデータを準備するステップが不安定で、テストしたいところの でテストが失敗することがある。例えば、以前のテスト実行時のデータが残っていたせいで、テストの準備段階で想定外のデータが存在していて、新たなデータの作成に失敗し、本来テストしたい場所に進めないことがある。他にも、GUIの表示が遅れたり、ページネーションの影響で想定どおりのデータが表示されない場合などにこのような状況が起きる。

僕はこの問題をテストの 独立性安定性 の問題だと捉え、次のようなアドバイスをしました。

  • テストを実行したことによる副作用は、テストがそれぞれ独立していないからだ。毎回ユニークなデータを生成するようにしたほうがいい。
  • テストデータを準備するステップではプログラマブルなインターフェース、例えばREST APIなどを別に準備したほうが良い。これは上述のユニークなテストデータの生成にも役に立つ。

すると、彼は続けて別の質問をしました。

「テストデータのためのAPIを準備したとして、そのAPIが本物のUIと同じ挙動をする保証はどこにあるのか?」

僕は、この問題がテストそのものではなく、 アプリケーションの設計 の問題だと考えました。そこで、次のような回答をしました。

「APIとUIが内部的に同じ動作をすることをアプリケーションの設計側で保証できないか?例えば、UIからリクエストされたときと、APIからリクエストされたときで、それぞれ同じクラスを呼ぶようにし、UIやAPIのコントローラー内のロジックは最低限にとどめればよいのではないか。これはアプリケーションの可読性やメンテナンス性に対しても良い影響を与えると思う。」

すると、彼は以下のような反論をしました。

「それは、現在ユーザー向けに提供しているUIの内部構造も含めたリファクタリングが必要になるということを意味するのではないか。だが、自動テストとは本来リファクタリングのような 内部実装の見直し を安全に進めるために必要なもので、内部実装を見直さないと良い自動テストが手に入らないのはおかしいのではないか。

それに、E2Eテストはビジネスプロセスをテストするものなのに、たとえテストのためであっても、実際のビジネスプロセスに登場しないAPIが出てくるのはおかしいと思う。」

2つの問いかけ

バーでの会話は、僕に2つの問いを投げかけました。

  1. そもそも安定して動く自動テストを作るためにテスト対象の内部に手を入れないといけないとなると、いつまでも理想的なテストが手に入らない、いわゆる、「卵が先か、鶏が先か」状態に陥ってしまう。
  2. 安定性を重視して前提条件や事後条件のセットにGUIの代わりにAPIを使ってしまうと、ビジネスプロセスをテストするというE2Eテストの要求を満たさないのではないか。

1つ目の問いは、そもそもなぜ 自動テストのために特別な実装が必要になるのか と言い換えられます。理想的な(=安定して動く、独立した、etc)自動テストを得るためにアプリケーション側に手を入れる必要があるのが、「卵が先か、鶏が先か」状態を生んでいるからです。本稿では、この特別な実装を テスタビリティ実装 と呼び、その必要性と重要性について説明します。

2つ目の問いは、ストレートに E2Eテストに対する要求は何か と言い換えられます。イントロダクションでは「ビジネスプロセスをテストするもの」と定義されていましたが、本稿ではこの定義を更に深く掘っていき、E2Eテストでは何をどのようにテストすべきかを探っていきます。

テスタビリティ実装の重要性

最初に、1つ目の問いに対する答えを探っていきましょう。その前に、そもそも自動テストとはどのような性質のもので、どのような特徴があるのかを改めて整理してみます。

テストコードはアプリケーションとの間に依存関係を作る

以下の例は、いわゆる FizzBuzz 問題の非常にシンプルな実装と、そのテストコードです。functionから始まる部分がコードの実装部分、下側の assert はテストコードに当たります。

テストコードは fizzBuzz 関数を実際に 使っている ことに着目してください。

Simple FizzBuzz implementation and assertion codes

例えば、この fizzBuzz 関数に意図的に不具合を仕込むとします。最後の return num を書かなかった場合、3と5の倍数以外では値を返さなくなるため、 fizzBuzz(1) および fizzBuzz(0) の結果は undefined となり、テストは失敗します。

Injected failure in the fizzBuzz. It will not return normal number anymore, and some assertions will fail

同様に、例えば if 文の並び順を変えるとどうなるでしょう。このケースでは、 fizzBuzz(15) の結果は Buzz となります。 155 の倍数でもあるので、 if (num % 5 === 0) が先に評価されるためです。

Injected failure that will not return FizzBuzz for the number multiplies 3 and 5.

さて、先に述べたように、 assertfizzBuzz を実際にコールしています。このことが意味するのは、 assertfizzBuzz に対して 依存関係 を作っているということになります。

また、5つの assert 文が表しているのは、 fizzBuzz 関数がどのような値を受け取った時に、どのような値を返すのかという 仕様 です。別の言い方をすれば、 テストコードとは、アプリケーションのインターフェースに依存した、仕様との互換性をチェックするコード です。

依存関係がある以上、この2つは切っても切れない関係にあります。言い換えれば、仕様と実装の間に依存関係を持たせて、切っても切れない関係にするのがテストコードです。そして、実装のインターフェースに依存している以上、テストコードの品質はアプリケーションの品質に大きく左右されます。ここで言う品質とは、テストコードの独立性や安定性など、テストコードそのものの質を指しています。

こうしたテストコードの質を左右するアプリケーションの品質特性は テスタビリティ と呼ばれており、この実装を本稿では テスタビリティ実装 と呼びます。これは、イントロダクションで登場した、テストのための内部的な変更や特別な実装が必要のことを指します。

テスタビリティ実装が必要になる理由

イントロダクションで紹介した例も含め、E2Eテストでは、テストデータの作成やクリーンアップなどにGUIを使うことが多いと思います。しかし、先述したように、テストコードはアプリケーションのインターフェースと依存関係を作ります。そして、E2Eテストにおいて、インターフェースとは GUI です。

E2Eテストでは、テストデータの準備も含めすべての操作をGUIを用いて行うことが多いと思います。しかしながら、それはテストと実装の間に 不要な依存関係を生む ことにつながります。

説明のために、あるECサイトにおける カートに入っている商品を購入確定する テスト を考えましょう。このテストでは、以下のような操作を必要とします。毎回商品や在庫、ユーザーなどを新たに登録しているのは、テストケースの 独立性 を強く意識しているためです。

テストケース: カートに入っている商品を購入確定する

  • 商品の作成
  • 在庫の登録
  • 新規ユーザーの作成
  • カートに入れる操作
  • ユーザーの発送先情報の登録
  • ユーザーの決済情報の登録
  • 購入を確定 操作

これらの手順のうち、本当にこのテストケースで テストしたいこと は何でしょうか?テストケース名は「カートに入っている商品を購入確定する」です。つまり、最後の「購入を確定」操作がテストの主目的で、その他の部分は テストデータの準備 に当たります。

こうした準備のためのステップにGUIを用いると、そのGUIに対して不要な依存関係が作られます。先の “テストコードはアプリケーションとの間に依存関係を作る” 節で説明したとおり、テストコードは実装との間に依存関係を作ることで仕様とのギャップを確認しますが、準備にGUIを使ってしまうと、本来テストしたい部分とは無関係の部分に対して依存関係を作ることにつながります。

一般的なアプリケーション設計のベストプラクティスとして、コンポーネント間の不要な依存関係は出来る限り避けるべきとされています。テストコードと実装の間でも全く同じことが言えるでしょう。

E2Eテストに対する要求

ビジネスプロセスに対するテスト

ところが、ここには大きな矛盾があります。

ISTQB の用語集の定義によれば、E2Eテストとはそもそも次のようなものだとされています。

本番相当の環境で、ビジネスプロセスを最初から最後まで実行するテストの一種。

つまり、E2Eテストには大前提として、ビジネスプロセスを最初から最後まで実行するテストであることが求められています。ビジネスプロセスの定義はISTQBの用語集には掲載されていませんが、 Wikipedia によれば、以下のように説明されています。

ビジネスプロセス: business process)とは、組織の目的を実現するために組織関係者(組織のメンバー)が行う一連のタスクや活動のことである。

先ほどのECサイトのテストを例に挙げると、以下のようなものがビジネスプロセスであると考えられます。複数のアクターとシステムが相互に作用しながら目的を実現していることが肝となります。ここでは、ECサイト(正確にはそのサイトの運営者)とエンドユーザーがそれぞれの目的に従ってシステム操作を行い、どのような価値が生まれるかを説明するのがビジネスプロセスです。また、それが達成されることを確認するのがビジネスプロセスに対するテストであると言えるでしょう。

ビジネスプロセス: 商品登録〜発送まで

ECサイトは商品を登録し、在庫を準備する。エンドユーザーはその商品を購入する。ECサイトはその商品を発送する。

ユーザーストーリーに対応するテスト

一方で、GUIを用いるテストは、果たしてビジネスプロセスのテストのみに限られるのでしょうか。例えば、 ユーザーストーリー はビジネスプロセスと同様に利用者目線での価値を表したドキュメントですが、一般的にビジネスプロセスほど大きくありません。先ほどのECサイトの例では、次のようなものがユーザーストーリーです。

ユーザーストーリーの例

エンドユーザーとして、私はカートに入った商品の注文を確定できる。

つまり、従来のE2Eテストを仮に ビジネスプロセスE2Eテスト と読んだとして、新たに ユーザーストーリーテスト という別の テストレベル が考えられます。

先述したように、ビジネスプロセスは一連のプロセスなので、データの準備なども含めてほぼすべてがGUIでの操作になるでしょう。一方で、ユーザーストーリーはビジネスプロセスの中の ごく一部 をユーザー目線で表現したドキュメントです。その中で語られていない、テストのための前提条件などは、APIなどプログラマブルな手段で準備したほうがより品質の高い、より独立し安定したテストになるでしょう。

リファクタリングが先か、テストが先か?

テストレベルとテスタビリティ実装

先述のテストレベルを表にまとめると上記の通りとなります。

それぞれのテストレベルは、テスト対象として何らかのインターフェースを利用しています。例えば、ビジネスプロセスE2EテストとユーザーストーリーE2EテストはいずれもUIを利用しています。結合テストや単体テストでは、システム内のコンポーネントやメソッドなどのインターフェースが使われるでしょう。これは、テストコードがこれらのインターフェースに依存し、想定通り動作するかどうかチェックすることを意味します。

一番右の テスタビリティ実装への依存度 という列は、そのテストレベルがどの程度テスタビリティ実装から影響を受けるかを意味します。例えば、ビジネスプロセスE2Eは原理的にはすべてがUIを用いて実行されるため、テスタビリティ実装はほとんど不要な場合が多いです。一方で、それは同時に 利用可能なテスタビリティ実装が非常に限られる ことも意味するため、テストの安定性、独立性を保つにはテスト設計側で考慮するべき部分が非常に多くなるでしょう。

逆に、単体テストなどはテスタビリティ実装への依存度が非常に高くなります。これは、設計の失敗でテストが不可能な関数などを想像してみるとわかりやすいでしょう。例えば、時刻を参照し、午前/午後のどちらかを返す関数のユニットテストを書く場合、その関数が内部的にOSの時刻を参照していると、テストコードは期待値を書くのが難しくなります。

This meridian function will return am or pm according to the OS timestamp.

これを解決するシンプルな手段は、現在時刻を引数として渡すことです。

上記の例ではテスタビリティ実装として 実装そのものをテスタブルにする というアプローチを取りましたが、テストレベルが低くなればなるほどこのような傾向が強まります。

本稿で新たに登場した ユーザーストーリーE2Eテスト はどうでしょうか?理屈の上で言えば、ユーザーストーリーE2EテストもビジネスプロセスE2Eテストと同様にすべてをGUIから操作できます。しかし、 “テスタビリティ実装が必要になる理由” の節で説明した通り、準備などのステップでGUIを用いてしまうと、安定性や独立性といったテストコードそのものの品質においてデメリットがあります。そのため、本稿ではこうした箇所にはプログラマブルなインターフェースを利用することを推奨します。

高いテストレベルからはじめて、徐々に降りていく

ここで、冒頭の2つの問いかけを振り返ってみましょう。

  1. そもそも安定して動く自動テストを作るためにテスト対象の内部に手を入れないといけないとなると、いつまでも理想的なテストが手に入らない、いわゆる、「卵が先か、鶏が先か」状態に陥ってしまう。
  2. 安定性を重視して前提条件や事後条件のセットにGUIの代わりにAPIを使ってしまうと、ビジネスプロセスをテストするというE2Eテストの要求を満たさないのではないか。

(2) については、E2Eテストの定義を少し拡張して、ユーザーストーリーレベルのE2Eテストという少し小さなスコープのテストを考えることで解決を図りました。

では、(1) についてはどうでしょうか?これについても、私たちはすでに答えに近いところまでたどり着いています。ポイントは、実装とテストは切っても切れない関係にあることと、テストにはレベルがあることです。

先ほどの表で、テストにはいくつかのレベルがあることと、テストレベルが低くなればなるほどテスタビリティ実装への依存度が高くなることを表しました。テスタビリティ実装への依存度はテストの安定性とトレードオフの関係にあり、言い換えれば低いテストレベルほどテストは一般的に安定して高速に動作することを表します。

そのため、実は (1) で語っている「理想的なテスト」というのは、実はあくまで「低いテストレベルのテスト」ということだけを意味していたのです。ですので、アプリケーション側にテスタビリティ実装が少なく – 別の言い方をすれば、アプリケーションのテスタビリティが低い場合は – より高いテストレベルからテストを始め、段階的により下のテストレベルに降りていくことで、「テスタビリティが低いからテストが書けない」という問題を回避できます。

まとめ

この記事では、次のような内容を説明させていただきました。

  • 自動テストコードはアプリケーション本体のコードと 依存関係 を作る
  • 一般的に、 不要な依存関係 を排除するのが良い設計と言える
  • 一方で、E2Eテストは GUIに対して強い依存関係 を作る
  • テストの準備などで GUIとの不要な依存関係 を作らないようにするのが重要
  • 不要な依存関係を減らすために、テストレベル を一つ落とす(ユーザーストーリーE2E
  • 低いテストレベルほど、 テスタビリティ実装への依存度 が高まる ≒テストしやすい設計、テストのためのAPIが欲しくなる
  • 低いテストレベルの実装が困難な場合は、高いテストレベルから始めて、順番に低いテストレベルも実装していく