テスト自動化の概要
まず最初に、ブラウザを本当に使用する必要があるかどうかを自問自答することから始めましょう。複雑なウェブアプリケーションに取り組んでいる場合、いつかはブラウザを開いて実際にテストする必要があるでしょう。
しかしながら、Selenium テストのような機能的なエンドユーザーテストは、実行にコストがかかります。さらに、効果的に実行するには、通常、大規模なインフラストラクチャを整備する必要があります。ユニットテストやより低レベルのアプローチなど、より軽量なテストアプローチを使用してテストできるかどうかを常に自問自答することが良いルールです。
ウェブブラウザのテスト事業に参入することを決定し、Selenium 環境がテストの作成を開始する準備ができたら、通常、次の 3 つのステップの組み合わせを実行します。
- データをセットアップする
- 一連の個別のアクションを実行する
- 結果を評価する
これらのステップはできるだけ短く保ちたいと思うでしょう。ほとんどの場合、1 つまたは 2 つの操作で十分なはずです。ブラウザ自動化は「不安定」であるという評判がありますが、実際には、ユーザーが頻繁に過剰な要求をするためです。後の章では、テストで表面化する断続的な問題を軽減するために使用できる手法、特にブラウザと WebDriver 間の競合状態を克服する方法について再び触れます。
テストを短く保ち、Web ブラウザを絶対に必要な場合にのみ使用することで、最小限の不安定さで多くのテストを行うことができます。
Selenium テストの明確な利点は、バックエンドからフロントエンドまで、アプリケーションのすべてのコンポーネントをユーザーの視点からテストできる固有の能力です。つまり、機能テストは実行にコストがかかるかもしれませんが、一度にビジネス上重要な大部分を網羅することもできます。
テスト要件
前述のように、Selenium テストは実行にコストがかかる場合があります。どの程度かは、テストを実行するブラウザによって異なりますが、歴史的にブラウザの動作は非常に多様であるため、複数のブラウザに対してクロステストを行うことが目標として掲げられることがよくありました。
Selenium を使用すると、複数のオペレーティングシステム上の複数のブラウザに対して同じ手順を実行できますが、考えられるすべてのブラウザ、その異なるバージョン、およびそれらが実行される多くのオペレーティングシステムを列挙すると、すぐに簡単な作業ではなくなります。
例から始めましょう
ラリーは、ユーザーがカスタムユニコーンを注文できるウェブサイトを作成しました。
一般的なワークフロー(ここでは「ハッピーパス」と呼びます)は、次のようになります。
- アカウントを作成する
- ユニコーンを構成する
- ショッピングカートに追加する
- チェックアウトして支払いを行う
- ユニコーンに関するフィードバックを提供する
これらすべての操作を実行する壮大な Selenium スクリプトを 1 つ書きたくなるかもしれませんが、多くの人が試みるでしょう。誘惑に抵抗してください! そうすると、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 行のコードは、このテストの最終的な目的であるユニコーンの構成から気をそらさないということです。
ページオブジェクトモデルの複雑さについては後の章で説明しますが、ここではその概念を紹介します。
テストは、サイト内のページというコンテキスト内で、ユーザーの視点から実行されるアクションで構成する必要があります。これらのページはオブジェクトとして保存され、ウェブページの構成方法とアクションの実行方法に関する特定の情報が含まれます。テスターとしてのあなたは、そのほとんどを気にする必要はありません。
どんなユニコーンが欲しいですか? ピンク色が良いかもしれませんが、必ずしもそうである必要はありません。最近、紫色が非常に人気があります。彼女にサングラスは必要ですか? 星のタトゥー? これらの選択は難しいですが、テスターとしてのあなたの主な関心事です。注文処理センターが適切なユニコーンを適切な人に送り出すことを保証する必要があり、それはこれらの選択から始まります。
その段落のどこにも、ボタン、フィールド、ドロップダウン、ラジオボタン、またはウェブフォームについては言及していません。テストもそうすべきではありません! 問題を解決しようとするユーザーのようにコードを書きたいと考えています。次に、これを行う 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 が気に入らなくなり、最新の Haskell バインディングと Fortran フロントエンドでサイト全体を再実装することにしたとしても、これらのテストレベルのコマンドを所定の位置に保持し、変更しないままにすることができます。
ページオブジェクトは、サイトの再設計に準拠するために多少のメンテナンスが必要になりますが、これらのテストは同じままになります。この基本的な設計を採用して、ブラウザに面するステップをできるだけ少なくしてワークフローを進めていく必要があります。次のワークフローでは、ユニコーンをショッピングカートに追加することになります。カートがその状態を適切に維持していることを確認するために、このテストを何度も繰り返したいと思うでしょう。開始する前にカートに複数のユニコーンがありますか? ショッピングカートにはいくつ入りますか? 同じ名前や機能で複数作成した場合、壊れますか? 既存のものを保持するだけですか、それとも別のものを追加しますか?
ワークフローを進むたびに、アカウントを作成したり、ユーザーとしてログインしたり、ユニコーンを構成したりする必要がないようにする必要があります。理想的には、API またはデータベースを介してアカウントを作成し、ユニコーンを事前構成できます。その後、ユーザーとしてログインし、Sparkles を見つけてカートに追加するだけです。
自動化すべきか、否か?
自動化は常に有利ですか? いつテストケースを自動化することを決定すべきですか?
テストケースを自動化することが常に有利とは限りません。手動テストの方が適切な場合もあります。たとえば、アプリケーションのユーザーインターフェースが近い将来大幅に変更される場合、自動化は結局書き直す必要があるかもしれません。また、テスト自動化を構築するのに十分な時間がない場合もあります。短期的には、手動テストの方が効果的な場合があります。アプリケーションに非常にタイトな締め切りがあり、現在利用可能なテスト自動化がなく、その時間枠内でテストを完了することが不可欠な場合、手動テストが最適なソリューションです。