i-whammy

プログラマーとして学んだこと、思考を整理したことを書く。はず

アプリケーションドライバレイヤーを利用した自動受け入れテストを考える

この記事は

『継続的デリバリー』を引き続き読み進める中で、表題のアプリケーションドライバレイヤー(及びウィンドウドライバパターン)を利用して受け入れテストを実装することについての記述に行き当たりました。

www.amazon.co.jp

以前の記事はこちら。

i-whammy.hatenablog.com

私は業務の中でGaugeとSelenideを利用したUIに対する自動テストを書く事で受け入れテストを実装しているのですが、保守性の高いテストを考える上でヒントになりそうだと思い、読んだ内容を自分なりに解釈しつつ記事にすることにしました。

この記事では、Kotlinのサンプルコードを用いて実装イメージとともに、『継続的デリバリー』で紹介されているアプリケーションドライバレイヤーという考え方について、掘り下げていきたいと思います。

受け入れテストにおけるレイヤー

『継続的デリバリー』の中で、筆者は受け入れテストを以下の3つのレイヤーに分けて説明しています。

  • 実行可能な受け入れ基準
  • テスト実装
  • アプリケーションドライバ

それぞれのレイヤの簡単な説明は以下の通りです。

  • 実行可能な受け入れ基準
    • 名の通り、受け入れテストにおける受け入れの基準を定める
    • 実行可能であることがポイントであり、Gaugeはこの点で親和性が高い
  • テスト実装
    • 実際に処理の呼び出しを行いテストの一連の流れをコントロールする
  • アプリケーションドライバ
    • 受け入れテストの対象となるシステムと直接やり取りをする

私はこの中でもアプリケーションドライバのレイヤーが自動テストの保守性を高める上で大きな役割を果たすのではないかと思いました。

以下でこのレイヤーについて掘り下げてみます。

アプリケーションドライバレイヤーについて

上記の通り、アプリケーションドライバレイヤーは受け入れテストの対象となるシステムと直接やり取りをするレイヤーであり、このレイヤーはどのようにテスト対象とやり取りをするかの詳細を知っていることになります。例えばUIに対するテストであればSelenium、Selenide等を利用してUIに対する操作を行います。

一方で他のレイヤーから見た際に、アプリケーションドライバレイヤーはドメイン言語で表現されたインターフェースを提供するという特徴を持ちます。

すなわち、テストの実装の側から見た場合には、テストの実装から、テストに必要な情報のやり取りについての詳細について知る事はなく、抽象度が高いAPIを通して詳細とやりとりをすることが出来るようになります。

因みにGUIに対するこのようなアプローチはウィンドウドライバパターンとして紹介されており、以下のファウラーの記事でも詳しく知ることができます。

martinfowler.com

なおアプリケーションドライバレイヤー自体はGUIに限らず、API等他のシステムをテスト対象とする場合でも利用できます。

アプリケーションドライバレイヤーのメリット

上記のような特徴を持つアプリケーションドライバレイヤーですが、以下のようなメリットがあると感じました。

保守性が高まる

『継続的デリバリー』内でも紹介されていますが、一番大きなメリットはテストが変更に対して強くなることだと思います。

特にUIに対するテストは、画面の細かな変更に対応するためにテストの修正を余儀なくされる場面が多くあるかと思います。

アプリケーションドライバレイヤーにUIとのやり取りの詳細を押し込めることで、変更に対して強いテストが書きやすくなります。

テストが失敗した際の対応が容易になる

『継続的デリバリー』の同じ章(8章 自動受け入れテスト)の他の節でも紹介されているのですが、適切なレイヤリングによってテストの問題の調査が容易になると考えます。 例えばSeleniumを用いた自動テストでは、テストが失敗する場合に「画面描画を待っていてタイムアウトしてしまった」「セレクタの指定が誤っていた」等の原因から、純粋なアサーションエラーまで幅広い原因が考えられます。

アプリケーションドライバレイヤーが適切に分離されている場合、どのような原因でテストが失敗したのか分かりやすくなり、また部分的にタイムアウトまでの時間を延長する際にも対応する箇所を局所化させることが出来るようになります。

他にもマルチデバイスへの対応がしやすくなる(デスクトップアプリ、Webアプリ、モバイル等)というメリットがあるかなと思いました。

実践

実際にアプリケーションドライバのレイヤーを利用して受け入れテストのリファクタリングを行っていく様子を見ていきたいと思います。

サンプルコードはKotlinとGauge、Selenideを利用して記載していますが、適宜コメントで補足を入れています。

テスト対象

簡単な銀行口座のアプリケーションを考えます。

ユーザーがアプリを立ち上げてログインした際に、現在の口座残高を知ることが出来たり、他の様々なメニューを開くことが出来るようなポータル画面が表示されるようなものをイメージしてください。

以下のようなコンポーネントがあり、そこにユーザーの現在の残高が表示されることをテストすることとします。

  <div>
    <p class="current-balance">{{currentBalance}}</p></div>

まずは愚直に

まずはシンプルに、現在の残高を確かめる実装を書いてみます。

// GaugeはMarkdown形式で書かれたSpecファイルとStepアノテーションが付けられた実装を紐付けることが出来ます
@Step("現在の残高が<balance>円である")
fun assertCurrentBalance(balance: Int) {
  // query selectorを利用して現在の残高を取得しアサーションする
  assertEquals(`$`(".current-balance"), balance)
}

簡潔な実装ですが、受け入れの基準を満たしているかどうかを検証するテストになっています。

一方で画面に対する変更があった場合、その影響を受けやすいコードです。

また「現在の残高(currentBalance)」ということは分かりますが、どのような場面、画面で利用されている残高なのかイメージがつきづらいです。

アプリケーションドライバを導入する

ではアプリケーションドライバレイヤーを導入してリファクタリングをしてみます。

@Step("現在の残高が<balance>円である")
fun assertCurrentBalance(balance: Int) {
  // UserPortalPageDriverというドライバクラスを用意する
  val currentBalance = userPortalPageDriver.getCurrentBalance()
  assertEquals(currentBalance, balance)
}

今回は UserPortalPageDriver というドライバクラスを設けて、ログイン時に初めに表示されるポータルページとの直接のやり取りを担う役割を持たせてみました。 これによって、ドライバクラス内にUIとのやり取りの詳細を隠蔽出来ました。

またクラス名からどのような場面で利用される残高なのかという点がはっきりし、ドメインとしての主張が強まったと思います。

『継続的デリバリー』内ではページ単位よりもさらに細かく、画面を構成するコンポーネント単位でドライバーを用意した例が掲載されていましたが、この辺りはアプリケーションの規模に応じて変えると良さそうだなと思いました。

まとめ

記事にしてみると、プロダクションコードを書く際には意識してきた抽象化、レイヤリングという要素をきちんとテストコードを書く際にも意識する事でより良いテストコードが書ける、という事を改めて実感できました。

『継続的デリバリー』はこの受け入れテストに関する8章でちょうど折り返しにあたるため、引き続き少しずつ読み進めていきたいと思います。