Webアプリケーションの自動E2Eテストのデザインパターン
はじめに
WebアプリケーションのE2Eでの総合テストもしくは、受け入れテストのテストレベルで用いられる自動テストのデザインパターンを調べてみたので、それらをまとめる。「Webアプリケーション」としたが、モバイルを含むネイティブアプリケーションやCLIアプリケーションにも通じるデザインパターンも含む様子。
Page Object Model Design Pattern
Page Object Model デザインパターンは、テスト対象のアプリケーションの画面を1つのオブジェクトとしてとらえるデザインパターン。
Page Object Model デザインパターンでは、アプリケーションの画面のインターフェースを提供するクラス(ページオブジェクト)とテストシナリオを記述するクラスとに分離される。それぞれのクラスの役割を以下に示す。
- ページオブジェクトクラス
- 画面やダイヤログ、画面の一要素などの単位で定義する。
- 画面の要素の保持する。
- 画面上での操作をするメソッドを提供する。
- アサーションは実施しない。ただし、対象のページであるかどうかの判定をインスタンス化するときにする。
- テストシナリオクラス
- ページオブジェクトクラスが提供するAPIを通じて画面を操作する。
- アサーションを実施する。
上記のようにそれぞれに役割をもつクラスに分離するため、以下のようなメリットがある。
- テストコードからDOM操作が排除され可読性が上がる。
- 同じDOM操作の処理を何度も書く必要がなくなり、コードの重複が減らせる。DRYの原則に従う。
- テスト対象のページに変更があった場合に、ページオブジェクトのみの変更で済む。
Page Object Model デザインパターンの原則をまとめると(Page object models - Seleniumのサマリを引用)
- ページオブジェクトが提供するメソッドはページが提供するサービスを表す
- ページ内部は、ページオブジェクトに閉じる。
- ページオブジェクトではアサーションをしない。
- メソッドは他のページオブジェクトを返す。
- ページオブジェクトは必ずしもページ全体を表す必要はない。
- 同じアクションに対して異なる結果になる場合は、異なるメソッドとしてモデル化する。
References of Page Object Model Pattern
- Page Object Pattern in Automated Testing
- Page object models - Selenium
- Page object models - Playwright
Facade Design Pattern
Page Object Modelデザインパターンを拡張したデザインパターン。Facade デザインパターンでは、ページオブジェクトクラスの数が増えて、それぞれのページオブジェクトとの関連した操作を含む複雑な処理が増えると扱うのが難しくなる。そこで、それらの複雑な処理を行う「窓口」の役割を担う、シンプルなインターフェースを提供するクラス(Facadeクラス)を用意する。Facadeクラスは、異なるページでの複数の操作を結合するメソッドをもつ。これにより複雑な操作を単純化できる。テストコードでは、Facadeクラスを呼び出すことになる。
Facade デザインパターンでは、以下のような構成となる。
- Facade クラス
- 複数のページオブジェクトクラス
- テストコード
注意点は、Facadeクラスで、処理をまとめすぎないこと。まとめすぎると、どんな操作をそのページで実施しているかを、テストコードから読み解けなくなる。
References of Facade Design Pattern
- Facade Design Pattern in Automated Testing
- Section of "Facade Design Pattern" in page of Design Patterns in Automation Framework
- Facadeパターン - TECHSCORE
Singleton Design Pattern
Singletonデザインパターンは、クラスのインスタンスが1つしか生成されない(インスタンス化されない)ことを担保するデザインパターンである。アプリケーション全体で統一しなければならない仕組みの実装に適用される。例えば、図書館の貸出記録帳のクラスは、インスタンスが一つでないと、とあるインスタンスでは本は在庫されているが、他のインスタンスでは貸出中などの状況が生じることを防ぐ。
自動テストにおいては、データベースにアクセスしたり、外部リソースを扱う場合にしばしば用いられる。また、Webドライバーのインスタンスをひとつに制限したい場合にも用いられる。
References of Singleton Design Pattern
- Singleton Design Pattern in Automated Testing
- Section of "Singleton Design Pattern" in page of Design Patterns in Automation Framework
- Singletonパターン - TECHSCORE
Fluent Page Object Model Design Pattern
Page Object Modelデザインパターンを拡張したデザインパターン。
オブジェクト指向プログラミングにおけるFluent Interfaceデザインパターンは、メソッドチェーンを使用して、コードの可読性を向上させるデザインパターンである。メソッドチェーンとは、メソッドがオブジェクト自身を返すことにより、複数のメソッド呼び出しを一連の操作として連鎖させることができる手法である。
Fluent Page Object Modelデザインパターンでは、Page Object ModelにFluent Interfaceを導入する。このデザインパターンでは、ページオブジェクトが提供するページ上での操作のためのメソッドは、オブジェクト自体、すなわちページオブジェクトを戻り値とし、メソッドチェーンを実現する。また、戻り値には、他のページのページオブジェクトをとっても良い。これにより、アプリケーションの一連の操作をメソッドチェーンを使用して書くことができ、テストにおいて、どのような操作をしているのかの可読性がよくなる。
References of Fluent Design Pattern
- Fluent Page Object Pattern in Automated Testing
- Section of "Fluent Page Object Model" in page of Design Patterns in Automation Framework
Strategy Design Pattern
オブジェクト指向プログラミングにおけるStrategyデザインパターンは、アルゴリズムの具体的な実装をカプセル化し、それらを独立したオブジェクトとして扱うことで、アルゴリズムを簡単に切り替えられるようにするデザインパターンである。クライアントは、インターフェイスとなるContextクラスを通じて、必要なStrategyクラスのオブジェクトを利用する。SOLIDの原則のOpen-Closeの原則に従った設計となる。
自動テストにおいては、Validationを行う部分をStrategyとして実装する。例えば、ECサイトでの商品購入時の料金のValidationについて考える。購入料金には、商品自体の料金、消費税、送料、ギフト料金などが含まれる。もしくは、通常価格や割引価格などもある。これらの金額の表示された値の正当性を判定するロジックは、異なる。それぞれの値の正当性を判定するクラスをStrategyクラスとして実装する。テストコード内では、Contextクラスから各Strategyクラスを呼び出す。以下には、通常価格と割引価格の2つのStrategyクラスの実装例を示す。
from abc import ABC, abstractmethod
class PricingStrategy(ABC):
@abstractmethod
def calculate_price(self, base_price: float) -> float:
pass
class NormalPricingStrategy(PricingStrategy):
def calculate_price(self, base_price: float):
return base_price
class DiscountPricingStrategy(PricingStrategy):
def calculate_price(self, base_price: float):
return base_price * 0.8 # 20% discount
class PricingContext:
def __init__(self, strategy: PricingStrategy):
self._strategy = strategy
def set_strategy(self, strategy: PricingStrategy):
self._strategy = strategy
def get_price(self, base_price: float) -> float:
return self._strategy.calculate_price(base_price)
if __name__ == "__main__":
product_base_price = 1000.0 # example base price
context = PricingContext(NormalPricingStrategy())
print("Normal price:", context.get_price(product_base_price))
context.set_strategy(DiscountPricingStrategy())
print("Discounted price:", context.get_price(product_base_price))
References of Strategy Design Pattern
ScreenPlay Pattern
SOLIDの原則に基づいて設計され、Page Object ModelパターンがSOLIDの原則に反する部分を解決する。Page Object Modelパターンでは、ページオブジェクトが、ページの要素や操作を保持している。したがって、ページに要素や操作が増える度に、ページオブジェクトが大きくなっていく。これは、しばしばSOLIDの原則、特にSingle Responsibilityの原則とOpen-Closedの原則に反する。Screenplayパターンでは、ページの構成とページの操作を分割したクラスとする。これにより、数は少ないけど大きなクラスではなく、数は多いが小さなクラスができる。一つ一つのクラスは、その責任範囲が単一になり、可読性が向上し、他のクラスとの依存が少なくなるため、メンテナンス性も向上する。したがって、Single Responsibilityの原則とOpen-Closedの原則に従うことになる。
Screenplayパターンでは、ユーザーがどう実行するかではなく、何を実行できるかに主眼を置いて、自動テストを書く。
Screenplayパターンにおけるテストシナリオの観点は以下のとおり。
- Who
このソフトウェアはどんな人が使うのか - Why
このソフトウェアをなぜ使うのか - What
Goalsを達成するために何をする必要があるのか - How
具体的にどのように操作するか
この観点に従って、自動テストを書く。このとき、ユーザーは何ができて、何を達成できるかについて考えるべきで、アプリケーションの実装や構造については、考えるべきではない。
上記の観点に対する現実世界での回答例は以下のフレームワークを構成する5つの要素によって解釈できる。
- Actors
テスト対象のソフトウェアを使う人。
e.g.) 図書館の利用者が本を探すために使う - Abilities
ソフトウェアのインターフェイスに接するための能力。 Actors が有する。
e.g.) ブラウザの画面上の要素に文字列を入力する能力 - Actions (Interactions)
Actors がインターフェイスを介して行う操作
e.g.) 検索画面の検索フィールドにに文字列を入力する - Tasks
Actions のひとまとまりにしたもの
e.g.) 書籍検索画面で本のタイトルで検索する (= 検索画面の検索フィールドに本のタイトルを入力する + 検索画面の検索ボタンを押下する) - Questions
テスト対象のソフトウェアの情報を取得する
e.g.) 検索した結果に本のタイトルが含まれていることを確認する
このように、「タスク」は宣言型、「アクション」は命令型と明確に区別する。
Screenplay pattern により、このような抽象化レベルを提供することで、より簡単に階層化したテストを書くことができるようになる。
ScreenplayパターンではActor
がRolesを担う。Actor
は、Ability
として、アプリケーションを実行可能である。ビジネス的な目標(上記の例では「本を探す」)の達成のために、Task
を遂行する。Action
は、Task
に対する具体的な操作を示す(上記の例では「検索画面の検索フィールドに本のタイトルを入力する」や「検索画面の検索ボタンを押下する」)。また、Actor
は、アプリケーションの状態をQuestion
として、確認する。上記の例で言えば、「検索結果の表示の確認」となる。
重要なことは、ユーザーの操作の定義とアプリケーションのモデリングの定義とが分離されていることである。
なお、アプリケーションのモデリングのクラスは、別途定義する必要がある。
Discussion