これは、このセクションの複数ページの印刷可能なビューです。 印刷するには、ここをクリックしてください.

このページの通常のビューに戻る.

ガイドラインとレコメンデーション

Some guidelines and recommendations on testing from the Selenium project.

Page being translated from English to Japanese. Do you speak Japanese? Help us to translate it by sending us pull requests!

「ベストプラクティス」に関するメモ:このドキュメントでは、“ベストプラクティス"というフレーズを意図的に避けています。 すべての状況に有効なアプローチはありません。 “ガイドラインとレコメンデーション"というアイデアを好みます。 これらを一通り読み、特定の環境でどのアプローチが効果的かを慎重に決定することをお勧めします。

機能テストは、多くの理由で適切に行うのが困難です。 まるでアプリケーションの状態、複雑さ、および依存関係が、テストを十分に難しくしないと思えるほど、ブラウザ(特にクロスブラウザの非互換性)を扱うのは、良いテストの作成を難しくします。

Seleniumは、機能的なユーザーインタラクションを簡単にするツールを提供しますが、適切に設計されたテストスイートの作成には役立ちません。 この章では、機能的なWebページの自動化に取り組む方法に関するアドバイス、ガイドライン、および推奨事項を提供します。

この章では、長年にわたって成功を収めてきたSeleniumの多くのユーザーの間で人気のあるソフトウェア設計パターンを記録します。

1 - テスト自動化について

まず、本当にブラウザを使用する必要があるかどうかを自問することから始めます。 ある時点で複雑なWebアプリケーションで作業している場合、おそらくブラウザを開いて実際にテストする必要があるでしょう。

ただし、Seleniumテストなどの機能的なエンドユーザーテストの実行には費用がかかります。 さらに、それらは通常、効果的に実行するために適切なインフラストラクチャを配置する必要があります。 単体テストなどのより軽量なテストアプローチを使用して、または下位レベルのアプローチを使用して、テストすることを実行できるかどうかを常に自問するのは良いルールです。

Webブラウザーのテストビジネスに参加していることを確認し、Selenium環境でテストの記述を開始できるようになったら、通常は3つのステップを組み合わせて実行します。

  • データを設定する
  • 個別の一連のアクションを実行する
  • 結果を評価する

これらの手順はできるだけ短くしてください。 ほとんどの場合、1つまたは2つの操作で十分です。 ブラウザの自動化は"不安定"であるという評判がありますが、実際には、ユーザーが頻繁に多くを求めることが多いためです。 後の章では、特にブラウザーとWebDriver間の競合状態を克服する方法に関する、テストでの断続的な問題を軽減するために使用できる手法に戻ります。

テストを短くして、代替手段がまったくない場合にのみWebブラウザーを使用することで、不安定さを最小限にして多くのテストを実行できます。

Seleniumテストの明確な利点は、ユーザーの観点から、バックエンドからフロントエンドまで、アプリケーションのすべてのコンポーネントをテストする固有の機能です。 つまり、機能テストは実行に費用がかかる可能性がありますが、同時にビジネスに不可欠な大規模な部分も含まれます。

テスト要件

前述のように、Seleniumテストの実行には費用がかかる場合があります。 どの程度までテストを実行しているブラウザーに依存しますが、歴史的にブラウザーの動作は非常に多様であるため、多くの場合、複数のブラウザーに対するクロステストの目標として述べられてきました。

Seleniumを使用すると、複数のオペレーティングシステム上の複数のブラウザーに対して同じ命令を実行できますが、すべての可能なブラウザー、それらの異なるバージョン、およびそれらが実行される多くのオペレーティングシステムの列挙はすぐに重要な作業になります。

例から始めましょう

ラリーは、ユーザーがカスタムユニコーンを注文できるWebサイトを作成しました。

一般的なワークフロー(“ハッピーパス"と呼ぶ)は次のようなものです。

  • アカウントを作成する
  • ユニコーンを設定する
  • ショッピングカートにユニコーンを追加します
  • チェックアウトしてお支払い
  • ユニコーンについてフィードバックを送る

これらのすべての操作を実行するために1つの壮大なSeleniumスクリプトを作成するのは魅力的です。 その誘惑に抵抗しましょう! そうすると、
a)時間がかかる
b)ページレンダリングのタイミングの問題に関する一般的な問題が発生する
c)失敗した場合、簡潔で"一目瞭然"にならない、何がうまくいかなかったかを診断する方法がない
というテストになります。

このシナリオをテストするための好ましい戦略は、一連の独立した迅速なテストに分割することです。 各テストには、1つの"理由"が存在します。

2番目のステップであるユニコーンの構成をテストしたいと思います。 次のアクションを実行します。

  • アカウントを作成する
  • ユニコーンを設定する

これらの手順の残りをスキップしていることに注意してください。 この手順を完了した後、他の小さな個別のテストケースで残りのワークフローをテストします。

開始するには、アカウントを作成する必要があります。 ここには、いくつかの選択があります。

  • 既存のアカウントを使用しますか?
  • 新しいアカウントを作成しますか?
  • 設定を開始する前に考慮する必要があるそのようなユーザーの特別なプロパティはありますか?

この質問への回答方法に関係なく、テストの"データのセットアップ"部分の一部にすると解決します。 ラリーが、ユーザー(またはだれでも)がユーザーアカウントを作成および更新できるAPIを公開している場合は、それを使用してこの質問に回答してください。 可能であれば、資格情報を使用してログインできるユーザーが"手元に"いる場合にのみブラウザを起動します。

各ワークフローの各テストがユーザーアカウントの作成から始まる場合、各テストの実行に何秒も追加されます。 APIの呼び出しとデータベースとの対話は、ブラウザを開いたり、適切なページに移動したり、フォームをクリックして送信されるのを待つなどの高価なプロセスを必要としない、迅速な"ヘッドレス"操作です。

理想的には、1行のコードでこのセットアップフェーズに対処できます。 これは、ブラウザーが起動する前に実行されます。

// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
User user = UserFactory.createCommonUser(); //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
AccountPage accountPage = loginAs(user.getEmail(), user.getPassword());
  
# Create a user who has read-only permissions--they can configure a unicorn,
# but they do not have payment information set up, nor do they have
# administrative privileges. At the time the user is created, its email
# address and password are randomly generated--you don't even need to
# know them.
user = user_factory.create_common_user() #This method is defined elsewhere.

# Log in as this user.
# Logging in on this site takes you to your personal "My Account" page, so the
# AccountPage object is returned by the loginAs method, allowing you to then
# perform actions from the AccountPage.
account_page = login_as(user.get_email(), user.get_password())
  
// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
User user = UserFactory.CreateCommonUser(); //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
AccountPage accountPage = LoginAs(user.Email, user.Password);
  
# Create a user who has read-only permissions--they can configure a unicorn,
# but they do not have payment information set up, nor do they have
# administrative privileges. At the time the user is created, its email
# address and password are randomly generated--you don't even need to
# know them.
user = UserFactory.create_common_user #This method is defined elsewhere.

# Log in as this user.
# Logging in on this site takes you to your personal "My Account" page, so the
# AccountPage object is returned by the loginAs method, allowing you to then
# perform actions from the AccountPage.
account_page = login_as(user.email, user.password)
  
// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
var user = userFactory.createCommonUser(); //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
var accountPage = loginAs(user.email, user.password);
  
// Create a user who has read-only permissions--they can configure a unicorn,
// but they do not have payment information set up, nor do they have
// administrative privileges. At the time the user is created, its email
// address and password are randomly generated--you don't even need to
// know them.
val user = UserFactory.createCommonUser() //This method is defined elsewhere.

// Log in as this user.
// Logging in on this site takes you to your personal "My Account" page, so the
// AccountPage object is returned by the loginAs method, allowing you to then
// perform actions from the AccountPage.
val accountPage = loginAs(user.getEmail(), user.getPassword())
  

ご想像のとおり、 UserFactory を拡張して createAdminUser()createUserWithPayment() などのメソッドを提供できます。 重要なのは、これらの2行のコードは、このテストの最終目的であるユニコーンの構成からあなたをそらすものではないということです。

ページオブジェクトモデルの込み入った事柄については、後の章で説明しますが、ここで概念を紹介します。

テストは、サイトのページのコンテキスト内で、ユーザーの観点から実行されるアクションで構成される必要があります。 これらのページはオブジェクトとして保存され、Webページがどのように構成され、アクションがどのように実行されるかに関する特定の情報が含まれます。

どんなユニコーンが欲しいですか? ピンクが必要かもしれませんが、必ずしもそうではありません。 紫は最近非常に人気があります。 彼女はサングラスが必要ですか? スタータトゥー? これらの選択は困難ですが、テスターとしての最大の関心事です。 発送センターが適切なユニコーンを適切な人に送信することを確認する必要があります。

この段落では、ボタン、フィールド、ドロップダウン、ラジオボタン、またはWebフォームについては説明していません。 また、テストするべきではありません! ユーザーが問題を解決しようとしているようにコードを書きたいと思います。 これを実行する1つの方法を次に示します(前の例から継続)

// The Unicorn is a top-level Object--it has attributes, which are set here. 
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
Unicorn sparkles = new Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS);

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.
AddUnicornPage addUnicornPage = accountPage.addUnicorn();

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
UnicornConfirmationPage unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles);
  
# The Unicorn is a top-level Object--it has attributes, which are set here.
# This only stores the values; it does not fill out any web forms or interact
# with the browser in any way.
sparkles = Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

# Since we're already "on" the account page, we have to use it to get to the
# actual place where you configure unicorns. Calling the "Add Unicorn" method
# takes us there.
add_unicorn_page = account_page.add_unicorn()

# Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
# its createUnicorn() method. This method will take Sparkles' attributes,
# fill out the form, and click submit.
unicorn_confirmation_page = add_unicorn_page.create_unicorn(sparkles)
  
// The Unicorn is a top-level Object--it has attributes, which are set here. 
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
Unicorn sparkles = new Unicorn("Sparkles", UnicornColors.Purple, UnicornAccessories.Sunglasses, UnicornAdornments.StarTattoos);

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.
AddUnicornPage addUnicornPage = accountPage.AddUnicorn();

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
UnicornConfirmationPage unicornConfirmationPage = addUnicornPage.CreateUnicorn(sparkles);
  
# The Unicorn is a top-level Object--it has attributes, which are set here.
# This only stores the values; it does not fill out any web forms or interact
# with the browser in any way.
sparkles = Unicorn.new('Sparkles', UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

# Since we're already "on" the account page, we have to use it to get to the
# actual place where you configure unicorns. Calling the "Add Unicorn" method
# takes us there.
add_unicorn_page = account_page.add_unicorn

# Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
# its createUnicorn() method. This method will take Sparkles' attributes,
# fill out the form, and click submit.
unicorn_confirmation_page = add_unicorn_page.create_unicorn(sparkles)
  
// The Unicorn is a top-level Object--it has attributes, which are set here.
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
var sparkles = new Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS);

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.

var addUnicornPage = accountPage.addUnicorn();

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
var unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles);

  
// The Unicorn is a top-level Object--it has attributes, which are set here. 
// This only stores the values; it does not fill out any web forms or interact
// with the browser in any way.
val sparkles = Unicorn("Sparkles", UnicornColors.PURPLE, UnicornAccessories.SUNGLASSES, UnicornAdornments.STAR_TATTOOS)

// Since we are already "on" the account page, we have to use it to get to the
// actual place where you configure unicorns. Calling the "Add Unicorn" method
// takes us there.
val addUnicornPage = accountPage.addUnicorn()

// Now that we're on the AddUnicornPage, we will pass the "sparkles" object to
// its createUnicorn() method. This method will take Sparkles' attributes,
// fill out the form, and click submit.
unicornConfirmationPage = addUnicornPage.createUnicorn(sparkles)

  

ユニコーンの設定が完了したら、ステップ3に進んで、ユニコーンが実際に機能することを確認する必要があります。

// The exists() method from UnicornConfirmationPage will take the Sparkles 
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
Assert.assertTrue("Sparkles should have been created, with all attributes intact", unicornConfirmationPage.exists(sparkles));
  
# The exists() method from UnicornConfirmationPage will take the Sparkles
# object--a specification of the attributes you want to see, and compare
# them with the fields on the page.
assert unicorn_confirmation_page.exists(sparkles), "Sparkles should have been created, with all attributes intact"
  
// The exists() method from UnicornConfirmationPage will take the Sparkles 
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
Assert.True(unicornConfirmationPage.Exists(sparkles), "Sparkles should have been created, with all attributes intact");
  
# The exists() method from UnicornConfirmationPage will take the Sparkles
# object--a specification of the attributes you want to see, and compare
# them with the fields on the page.
expect(unicorn_confirmation_page.exists?(sparkles)).to be, 'Sparkles should have been created, with all attributes intact'
  
// The exists() method from UnicornConfirmationPage will take the Sparkles
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
assert(unicornConfirmationPage.exists(sparkles), "Sparkles should have been created, with all attributes intact");

  
// The exists() method from UnicornConfirmationPage will take the Sparkles 
// object--a specification of the attributes you want to see, and compare
// them with the fields on the page.
assertTrue("Sparkles should have been created, with all attributes intact", unicornConfirmationPage.exists(sparkles))
  

テスターはまだこのコードでユニコーンについて話しているだけです。 ボタンもロケーターもブラウザーコントロールもありません。 ラリーが来週、Ruby-on-Railsが好きではなくなったと判断し、Fortranフロントエンドを使用して最新のHaskellバインディングでサイト全体を再実装することを決めた場合でも、アプリケーションを モデル化する この方法により、これらのテストレベルのコマンドを所定の位置に変えずに維持できます。

ページオブジェクトは、サイトの再設計に準拠するために若干のメンテナンスが必要になりますが、これらのテストは同じままです。 この基本的な設計を採用することで、可能な限りブラウザに面した最小限の手順でワークフローを進めていきたいと思うでしょう。 次のワークフローでは、ユニコーンをショッピングカートに追加します。 カートの状態が適切に維持されていることを確認するために、おそらくこのテストを何度も繰り返す必要があります。 開始する前に、カートに複数のユニコーンがありますか? ショッピングカートには何個収容できますか? 同じ名前や機能で複数作成すると、壊れますか? 既存のものを保持するだけですか、それとも別のものを追加しますか?

ワークフローを移動するたびに、アカウントを作成し、ユーザーとしてログインし、ユニコーンを設定する必要を避けたいと考えています。 理想的には、APIまたはデータベースを介してアカウントを作成し、ユニコーンを事前設定できるようになります。 その後、ユーザーとしてログインし、きらめきを見つけてカートに追加するだけです。

自動化するかしないか

自動化は常に有利ですか? テストケースの自動化をいつ決定する必要がありますか?

テストケースを自動化することは必ずしも有利ではありません。 手動テストがより適切な場合があります。 たとえば、近い将来にアプリケーションのユーザーインターフェースが大幅に変更される場合は、自動化を書き換える必要があるかもしれません。 また、テストの自動化を構築する時間が足りない場合もあります。 短期的には、手動テストの方が効果的です。 アプリケーションの期限が非常に厳しい場合、現在利用できるテストの自動化はなく、その期間内にテストを実施することが不可欠です。 手動テストが最適なソリューションです。

2 - テストの種類

受け入れテスト

このタイプのテストは、機能またはシステムが顧客の期待と要件を満たしているかどうかを判断するために行われます。 このタイプのテストには通常、顧客の協力またはフィードバックが関与します。 下記質問に答えることで確認することができます。

正しい 製品を作っていますか?

Webアプリケーションの場合、ユーザーの予想される動作をシミュレートすることで、 このテストの自動化をSeleniumで直接実行できます。 このシミュレーションは、このドキュメントで説明されているように、記録/再生によって、 またはサポートされているさまざまな言語によって実行できます。 注:受け入れテストは 機能テスト のサブタイプであり、一部の人はこれにも言及する場合があります。

機能テスト

このタイプのテストは、機能またはシステムが問題なく正常に機能するかどうかを判断するために行われます。 システムをさまざまなレベルでチェックして、すべてのシナリオがカバーされていること、 およびシステムが実行すべきことを実行していることを確認します。 下記質問に答えることで確認することができます。

製品を 正しく 作っていますか?

これは通常以下を含みます。 テストがエラーなし(404、例外…)、使用可能な方法(正しいリダイレクト)で機能する、 利用しやすく、仕様に一致します(上記の 受け入れテスト を参照)。

Webアプリケーションの場合、期待されるリターンをシミュレートすることにより、このテストの自動化をSeleniumで直接実行できます。 このシミュレーションは、このドキュメントで説明されているように、記録/再生またはサポートされているさまざまな言語で実行できます。

パフォーマンステスト

その名前が示すように、パフォーマンステストは、アプリケーションのパフォーマンスを測定するために行われます。

パフォーマンステストには2つの主なサブタイプがあります。

ロードテスト

ロードテストは、定義されたさまざまな負荷(通常、特定の数のユーザーが同時に接続されている場合)でアプリケーションがどの程度機能するかを確認するために行われます。

ストレステスト

ストレステストは、ストレス下(またはサポートされている最大負荷以上)でアプリケーションがどの程度機能するかを確認するために行われます。

一般に、パフォーマンステストは、Seleniumで書かれたテストを実行して、さまざまなユーザーがWebアプリの特定の機能を押して、意味のある測定値を取得することをシミュレートして実行されます。

これは通常、メトリックを取得する他のツールによって行われます。 そのようなツールの1つが JMeter です。

Webアプリケーションの場合、測定する詳細には、スループット、待ち時間、データ損失、個々のコンポーネントの読み込み時間などが含まれます…

注1:すべてのブラウザには、開発者のツールセクションにパフォーマンスタブがあります(F12キーを押すとアクセス可能)

注2:これは一般に機能/機能ごとではなくシステムごとに測定されるため、 非機能テスト のサブタイプです。

回帰テスト

このテストは通常、変更、修正、または機能の追加後に行われます。

変更によって既存の機能が破壊されないようにするために、すでに実行されたいくつかのテストが再度実行されます。

再実行されるテストのセットは、完全または部分的なものにすることができ、アプリケーションおよび開発チームに応じて、いくつかの異なるタイプを含めることができます。

テスト駆動開発 (TDD)

テストタイプそのものではなく、TDDはテストが機能の設計を推進する反復的な開発方法論です。

各サイクルは、機能がパスする単体テストのセットを作成することから始まります(最初に実行すると失敗します)。

この後、テストに合格するための開発が行われます。 別のサイクルを開始してテストが再度実行され、すべてのテストに合格するまでこのプロセスが続行されます。

これは、欠陥が発見されるほどコストが安くなるという事実に基づいて、アプリケーションの開発をスピードアップすることを目的としています。

ビヘイビア駆動開発 (BDD)

BDDは、上記に基づいた反復開発方法論(TDD)でもあり、その目的は、アプリケーションの開発にすべての関係者を関与させることです。

各サイクルは、いくつかの仕様を作成することから始まります(これは失敗するはずです)。 次に、失敗する単体テスト(これも失敗するはずです)を作成し、開発を作成します。

このサイクルは、すべてのタイプのテストに合格するまで繰り返されます。

そのためには、仕様言語が使用されます。 すべての関係者が理解でき、単純で、標準的かつ明示的でなければなりません。 ほとんどのツールは、この言語として Gherkin を使用します。

目標は、潜在的な受入エラーも対象とすることでTDDよりも多くのエラーを検出し、当事者間のコミュニケーションを円滑にすることです。

現在、仕様を記述し、 CucumberSpecFlow などのコード関数と一致させるための一連のツールが利用可能です。

Selenium上に一連のツールが構築されており、BDD仕様を実行可能コードに直接変換することにより、このプロセスをさらに高速化しています。 これらのいくつかは、 JBehave、Capybara、およびRobot Framework です。

3 - ページオブジェクトモデル

ページオブジェクトは、テストメンテナンスを強化し、コードの重複を減らすためのテスト自動化で一般的になったデザインパターンです。 ページオブジェクトは、AUT(テスト対象アプリケーション)のページへのインターフェイスとして機能するオブジェクト指向クラスです。 テストは、そのページのUIと対話する必要があるときは常に、このページオブジェクトクラスのメソッドを使用します。 利点は、ページのUIが変更された場合、テスト自体を変更する必要はなく、ページオブジェクト内のコードのみを変更する必要があることです。 その後、その新しいUIをサポートするためのすべての変更は1か所に配置されます。

ページオブジェクトデザインパターンには、次の利点があります。

  • テストコードと、ロケーター(またはUIマップを使用している場合はロケーター)、レイアウトなどのページ固有のコードを明確に分離します。
  • これらのサービスをテスト全体に分散させるのではなく、ページによって提供されるサービスまたは操作用の単一のリポジトリがあります。

どちらの場合でも、これにより、UIの変更により必要な変更をすべて1か所で行うことができます。 この’テストデザインパターン’が広く使用されるようになったため、この手法に関する有用な情報は多数のブログで見つけることができます。 詳細を知りたい読者には、このテーマに関するブログをインターネットで検索することをお勧めします。 多くの人がこの設計パターンについて書いており、このユーザーガイドの範囲を超えた有用なヒントを提供できます。 ただし、簡単に始めるために、ページオブジェクトを簡単な例で説明します。

最初に、ページオブジェクトを使用しないテスト自動化の典型的な例を考えてみましょう。

/***
 * Tests login feature
 */
public class Login {

  public void testLogin() {
    // fill login data on sign-in page
    driver.findElement(By.name("user_name")).sendKeys("testUser");
    driver.findElement(By.name("password")).sendKeys("my supersecret password");
    driver.findElement(By.name("sign_in")).click();

    // verify h1 tag is "Hello userName" after login
    driver.findElement(By.tagName("h1")).isDisplayed();
    assertThat(driver.findElement(By.tagName("h1")).getText(), is("Hello userName"));
  }
}

このアプローチには2つの問題があります。

  • テスト方法とAUTのロケーター(この例ではID)の間に区別はありません。 どちらも単一のメソッドで絡み合っています。 AUTのUIが識別子、レイアウト、またはログインの入力および処理方法を変更する場合、テスト自体を変更する必要があります。
  • IDロケーターは、このログインページを使用する必要があったすべてのテストで、複数のテストに分散されます。

ページオブジェクトの手法を適用すると、この例は、サインインページのページオブジェクトの次の例のように書き換えることができます。

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Sign-in page.
 */
public class SignInPage {
  protected WebDriver driver;

  // <input name="user_name" type="text" value="">
  private By usernameBy = By.name("user_name");
  // <input name="password" type="password" value="">
  private By passwordBy = By.name("password");
  // <input name="sign_in" type="submit" value="SignIn">
  private By signinBy = By.name("sign_in");

  public SignInPage(WebDriver driver){
    this.driver = driver;
  }

  /**
    * Login as valid user
    *
    * @param userName
    * @param password
    * @return HomePage object
    */
  public HomePage loginValidUser(String userName, String password) {
    driver.findElement(usernameBy).sendKeys(userName);
    driver.findElement(passwordBy).sendKeys(password);
    driver.findElement(signinBy).click();
    return new HomePage(driver);
  }
}

そして、ホームページのページオブジェクトは次のようになります。

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

/**
 * Page Object encapsulates the Home Page
 */
public class HomePage {
  protected WebDriver driver;

  // <h1>Hello userName</h1>
  private By messageBy = By.tagName("h1");

  public HomePage(WebDriver driver){
    this.driver = driver;
    if (!driver.getTitle().equals("Home Page of logged in user")) {
      throw new IllegalStateException("This is not Home Page of logged in user," +
            " current page is: " + driver.getCurrentUrl());
    }
  }

  /**
    * Get message (h1 tag)
    *
    * @return String message text
    */
  public String getMessageText() {
    return driver.findElement(messageBy).getText();
  }

  public HomePage manageProfile() {
    // Page encapsulation to manage profile functionality
    return new HomePage(driver);
  }
  /* More methods offering the services represented by Home Page
  of Logged User. These methods in turn might return more Page Objects
  for example click on Compose mail button could return ComposeMail class object */
}

したがって、ログインテストでは、これら2つのページオブジェクトを次のように使用します。

/***
 * Tests login feature
 */
public class TestLogin {

  @Test
  public void testLogin() {
    SignInPage signInPage = new SignInPage(driver);
    HomePage homePage = signInPage.loginValidUser("userName", "password");
    assertThat(homePage.getMessageText(), is("Hello userName"));
  }

}

ページオブジェクトの設計方法には多くの柔軟性がありますが、テストコードの望ましい保守性を得るための基本的なルールがいくつかあります。

ページオブジェクト自体は、検証やアサーションを行うべきではありません。 これはテストの一部であり、常にページオブジェクトではなく、テストのコード内にある必要があります。 ページオブジェクトには、ページの表現と、ページがメソッドを介して提供するサービスが含まれますが、テスト対象に関連するコードはページオブジェクト内に存在しないようにします。

ページオブジェクト内に存在する可能性のある単一の検証があります。 これは、ページおよびページ上の重要な要素が正しく読み込まれたことを検証するためのものです。 この検証は、ページオブジェクトをインスタンス化する間に実行する必要があります。 上記の例では、SignInPageコンストラクターとHomePageコンストラクターの両方が期待するページを取得し、テストからの要求に対応できることを確認します。

ページオブジェクトは、必ずしもページ全体を表す必要はありません。 ページオブジェクトデザインパターンは、ページ上のコンポーネントを表すために使用できます。 AUTのページに複数のコンポーネントがある場合、コンポーネントごとに個別のページオブジェクトがあると、保守性が向上する場合があります。

また、テストで使用できる他のデザインパターンがあります。 ページファクトリを使用してページオブジェクトをインスタンス化するものもあります。 これらすべてについて議論することは、このユーザーガイドの範囲を超えています。 ここでは、読者にできることのいくつかを認識させるための概念を紹介したいだけです。 前述のように、多くの人がこのトピックについてブログを書いていますし、読者がこれらのトピックに関するブログを検索することをお勧めします。

4 - ドメイン固有言語(DSL)

ドメイン固有言語(DSL)は、問題を解決するための表現手段をユーザーに提供するシステムです。 それによって、ユーザーは、プログラマーの言葉でなく、自分の言葉でシステムとやりとりすることができます。

通常、ユーザーはサイトの外観を気にしません。 装飾、アニメーション、グラフィックスは気にしません。 彼らはあなたのシステムを使用して、新しい従業員を最小限の難しさでプロセスに押し込みたいと考えています。 彼らはアラスカへの旅行を予約したい。 ユニコーンを設定して割引価格で購入したいのです。 テスターとしてのあなたの仕事は、この考え方を"とらえる"ことにできるだけ近づくことです。 それを念頭に置いて、テストスクリプト(ユーザーの唯一のプレリリースの代理人)がユーザーを"代弁し"、表現するように、作業中のアプリケーションの"モデリング"に取り掛かります。

Seleniumでは、DSLは通常、APIをシンプルで読みやすいように記述したメソッドで表されます。 開発者と利害関係者(ユーザー、製品所有者、ビジネスインテリジェンススペシャリストなど)との伝達が可能になります。

利点

  • Readable: ビジネス関係者はそれを理解できます。
  • Writable: 書きやすく、不要な重複を避けます。
  • Extensible: 機能は(合理的に)契約と既存の機能を壊すことなく追加できます。
  • Maintainable: 実装の詳細をテストケースから除外することにより、AUT* の変更に対して十分に隔離されます。

Java

Javaの妥当なDSLメソッドの例を次に示します。 簡潔にするために、driverオブジェクトが事前に定義されており、メソッドで使用可能であることを前提としています。

/**
 * Takes a username and password, fills out the fields, and clicks "login".
 * @return An instance of the AccountPage
 */
public AccountPage loginAsUser(String username, String password) {
  WebElement loginField = driver.findElement(By.id("loginField"));
  loginField.clear();
  loginField.sendKeys(username);

  // Fill out the password field. The locator we're using is "By.id", and we should
  // have it defined elsewhere in the class.
  WebElement passwordField = driver.findElement(By.id("password"));
  passwordField.clear();
  passwordField.sendKeys(password);

  // Click the login button, which happens to have the id "submit".
  driver.findElement(By.id("submit")).click();

  // Create and return a new instance of the AccountPage (via the built-in Selenium
  // PageFactory).
  return PageFactory.newInstance(AccountPage.class);
}

このメソッドは、テストコードから入力フィールド、ボタン、クリック、さらにはページの概念を完全に抽象化します。 このアプローチを使用すると、テスターはこのメソッドを呼び出すだけで済みます。 これにより、メンテナンスの利点が得られます。 ログインフィールドが変更された場合、テストではなく、このメソッドを変更するだけで済みます。

public void loginTest() {
    loginAsUser("cbrown", "cl0wn3");

    // Now that we're logged in, do some other stuff--since we used a DSL to support
    // our testers, it's as easy as choosing from available methods.
    do.something();
    do.somethingElse();
    Assert.assertTrue("Something should have been done!", something.wasDone());

    // Note that we still haven't referred to a button or web control anywhere in this
    // script...
}

繰り返しになります。 主な目標の1つは、 テストが UIの問題ではなく、手元の問題 に対処できるAPIを作成することです。 UIはユーザーにとって二次的な関心事です。ユーザーはUIを気にせず、ただ仕事をやりたいだけです。 テストスクリプトは、ユーザーがやりたいことと知りたいことの長々としたリストのように読む必要があります。 テストでは、UIがどのようにそれを実行するように要求するかについて、気にするべきではありません。

*AUT: Application under test(テスト対象アプリケーション)

5 - モック外部サービス

外部サービスへの依存を排除すると、テストの速度と安定性が大幅に向上します。

6 - アプリケーション状態の生成

Seleniumはテストケースの準備に使用しないでください。 テストケースのすべての反復アクションと準備は、他の方法で行う必要があります。 たとえば、ほとんどのWeb UIには認証があります(ログインフォームなど)。 すべてのテストの前にWebブラウザーからのログインをなくすことで、テストの速度と安定性の両方が向上します。 AUT* にアクセスするためのメソッドを作成する必要があります(APIを使用してログインし、Cookieを設定するなど)。 また、テスト用にデータをプリロードするメソッドの作成は、Seleniumを使用して実行しないほうがいいです。 前述のように、AUT* のデータを作成するには、既存のAPIを活用する必要があります。

*AUT: Application under test(テスト対象アプリケーション)

7 - 改善されたレポート

Seleniumは、実行されたテストケースのステータスをレポートするようには設計されていません。 単体テストフレームワークの組み込みのレポート機能を利用することは、良いスタートです。 ほとんどの単体テストフレームワークには、xUnitまたはHTML形式のレポートを生成できるレポートがあります。 xUnitレポートは、Jenkins、Travis、Bambooなどの継続的インテグレーション(CI)サーバーに結果をインポートするのに人気があります。 いくつかの言語のレポート出力に関する詳細情報へのリンクがあります。

NUnit 3 Console Runner

NUnit 3 Console Command Line

xUnit getting test results in TeamCity

xUnit getting test results in CruiseControl.NET

xUnit getting test results in Azure DevOps

8 - 状態を共有しない

いくつかの場所で言及されていますが、再度言及する価値があります。 テストが互いに分離されていることを確認してください。

  • テストデータを共有しないでください。 アクションを実行する1つを選択する前に、それぞれが有効な注文をデータベースに照会するいくつかのテストを想像してください。 2つのテストで同じ順序を選択すると、予期しない動作が発生する可能性があります。

  • 別のテストで取得される可能性のあるアプリケーション内の古いデータを削除します。 例: 無効な注文レコード

  • テストごとに新しいWebDriverインスタンスを作成します。 これにより、テストの分離が保証され、並列化がより簡単になります。

9 - テストごとに新しいブラウザを起動する

クリーンな既知の状態から各テストを開始します。 理想的には、テストごとに新しい仮想マシンを起動します。 新しい仮想マシンの起動が実用的でない場合は、少なくともテストごとに新しいWebDriverを起動してください。 Firefoxの場合、既知のプロファイルでWebDriverを起動します。

FirefoxProfile profile = new FirefoxProfile(new File("pathToFirefoxProfile"));
WebDriver driver = new FirefoxDriver(profile);

10 - テストの独立性

各テストを独自のユニットとして記述します。 他のテストに依存しない方法でテストを記述してください。

公開後にモジュールとしてWebサイトに表示されるカスタムコンテンツを作成できるコンテンツ管理システム(CMS)があり、CMSとアプリケーション間の同期に時間がかかる場合があるとします。

モジュールをテストする間違った方法は、1つのテストでコンテンツが作成および公開され、別のテストでモジュールをチェックすることです。 コンテンツは公開後、他のテストですぐに利用できない可能性があるため、この方法はふさわしくありません。

代わりに、影響を受けるテスト内でオン/オフできるスタブコンテンツを作成し、それをモジュールの検証に使用できます。 ただし、コンテンツの作成については、別のテストを行うことができます。

11 - Fluent APIの使用を検討する

マーチン・ファウラーは“Fluent API”という用語を作り出しました。 Seleniumは既に、FluentWaitクラスでこのようなものを実装しています。 これは、標準のWaitクラスの代替としてのものです。 ページオブジェクトでFluent APIデザインパターンを有効にしてから、次のようなコードスニペットを使用してGoogle検索ページを照会できます。

driver.get( "http://www.google.com/webhp?hl=en&amp;tab=ww" );
GoogleSearchPage gsp = new GoogleSearchPage();
gsp.withFluent().setSearchString().clickSearchButton();

この流暢な動作を持つGoogleページオブジェクトクラスは次のようになります。

public class GoogleSearchPage extends LoadableComponent<GoogleSearchPage> {
  private final WebDriver driver;
  private GSPFluentInterface gspfi;

  public class GSPFluentInterface {
    private GoogleSearchPage gsp;

    public GSPFluentInterface(GoogleSearchPage googleSearchPage) {
        gsp = googleSearchPage;
    }

    public GSPFluentInterface clickSearchButton() {
        gsp.searchButton.click();
        return this;
    }

    public GSPFluentInterface setSearchString( String sstr ) {
        clearAndType( gsp.searchField, sstr );
        return this;
    }
  }

  @FindBy(id = "gbqfq") private WebElement searchField;
  @FindBy(id = "gbqfb") private WebElement searchButton;
  public GoogleSearchPage(WebDriver driver) {
    gspfi = new GSPFluentInterface( this );
    this.get(); // If load() fails, calls isLoaded() until page is finished loading
    PageFactory.initElements(driver, this); // Initialize WebElements on page
  }

  public GSPFluentInterface withFluent() {
    return gspfi;
  }

  public void clickSearchButton() {
    searchButton.click();
  }

  public void setSearchString( String sstr ) {
    clearAndType( searchField, sstr );
  }

  @Override
  protected void isLoaded() throws Error {
    Assert.assertTrue("Google search page is not yet loaded.", isSearchFieldVisible() );
  }

  @Override
  protected void load() {
    if ( isSFieldPresent ) {
      Wait<WebDriver> wait = new WebDriverWait( driver, Duration.ofSeconds(3) );
      wait.until( visibilityOfElementLocated( By.id("gbqfq") ) ).click();
    }
  }
}