テストの実践
Seleniumプロジェクトからのテストに関するいくつかのガイドラインと推奨事項
「ベストプラクティス」に関するメモ:このドキュメントでは、“ベストプラクティス"というフレーズを意図的に避けています。
すべての状況に有効なアプローチはありません。
“ガイドラインとレコメンデーション"というアイデアを好みます。
これらを一通り読み、特定の環境でどのアプローチが効果的かを慎重に決定することをお勧めします。
機能テストは、多くの理由で適切に行うのが困難です。
まるでアプリケーションの状態、複雑さ、および依存関係が、テストを十分に難しくしないと思えるほど、ブラウザ(特にクロスブラウザの非互換性)を扱うのは、良いテストの作成を難しくします。
Seleniumは、機能的なユーザーインタラクションを簡単にするツールを提供しますが、適切に設計されたテストスイートの作成には役立ちません。
この章では、機能的なWebページの自動化に取り組む方法に関するアドバイス、ガイドライン、および推奨事項を提供します。
この章では、長年にわたって成功を収めてきたSeleniumの多くのユーザーの間で人気のあるソフトウェア設計パターンを記録します。
1 - デザインパターンと開発戦略
(以前の場所: https://github.com/SeleniumHQ/selenium/wiki/Bot-Style-Tests)
概要
時間の経過とともに、プロジェクトは多数のテストが積み上がる傾向があります。
テストの総数が増えると、コードベースに変更を加えることが難しくなります。
アプリケーションが正常に機能していても、1回の"単純な"変更で多数のテストが失敗する可能性があります。
これらの問題が避けられない場合もありますが、問題が発生した場合は、できるだけ早く稼働を再開する必要があります。
次のデザインパターンと戦略は、テストの作成と保守を容易にするためにWebDriverで以前に使用されています。
それらもあなたにとって役に立つかもしれません。
DomainDrivenDesign:アプリのエンドユーザーの言語でテストを表現します。
PageObjects:WebアプリのUIの単純な抽象化
LoadableComponent:PageObjectsをコンポーネントとしてモデリングします。
BotStyleTests:PageObjectsが推奨するオブジェクトベースのアプローチではなく、コマンドベースのアプローチを使用してテストを自動化します。
ロード可能なコンポーネント
それは何ですか?
LoadableComponentは、PageObjectsの作成の負担を軽減することを目的としたベースクラスです。
これは、ページがロードされることを保証する標準的な方法を提供し、ページのロードの失敗のデバッグを容易にするフックを提供することによってこれを行います。
これを使用して、テストの定型コードの量を減らすことができます。これにより、テストの保守が面倒になります。
現在、Selenium 2の一部として出荷されるJavaの実装がありますが、使用されるアプローチは、どの言語でも実装できるほど単純です。
簡単な使用方法
モデル化するUIの例として、新しいissueのページをご覧ください。
テスト作成者の観点から、これは新しい問題を提出できるサービスを提供します。
基本的なページオブジェクトは次のようになります。
package com.example.webdriver;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
public class EditIssue {
private final WebDriver driver;
public EditIssue(WebDriver driver) {
this.driver = driver;
}
public void setSummary(String summary) {
WebElement field = driver.findElement(By.name("summary"));
clearAndType(field, summary);
}
public void enterDescription(String description) {
WebElement field = driver.findElement(By.name("comment"));
clearAndType(field, description);
}
public IssueList submit() {
driver.findElement(By.id("submit")).click();
return new IssueList(driver);
}
private void clearAndType(WebElement field, String text) {
field.clear();
field.sendKeys(text);
}
}
これをLoadableComponentに変換するには、これを基本型として設定するだけです。
public class EditIssue extends LoadableComponent<EditIssue> {
// rest of class ignored for now
}
この署名は少し変わっているように見えますが、それは、このクラスがEditIssueページをロードするLoadableComponentを表すことを意味します。
このベースクラスを拡張することにより、2つの新しいメソッドを実装する必要があります。
@Override
protected void load() {
driver.get("https://github.com/SeleniumHQ/selenium/issues/new");
}
@Override
protected void isLoaded() throws Error {
String url = driver.getCurrentUrl();
assertTrue("Not on the issue entry page: " + url, url.endsWith("/new"));
}
load
メソッドはページに移動するために使用され、 isLoaded
メソッドは正しいページにいるかどうかを判断するために使用されます。
このメソッドはブール値を返す必要があるように見えますが、代わりにJUnitのAssertクラスを使用して一連のアサーションを実行します。
アサーションは好きなだけ少なくても多くてもかまいません。
これらのアサーションを使用することで、クラスのユーザーにテストのデバッグに使用できる明確な情報を提供することができます。
少し手直しすると、PageObjectは次のようになります。
package com.example.webdriver;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import static junit.framework.Assert.assertTrue;
public class EditIssue extends LoadableComponent<EditIssue> {
private final WebDriver driver;
// By default the PageFactory will locate elements with the same name or id
// as the field. Since the summary element has a name attribute of "summary"
// we don't need any additional annotations.
private WebElement summary;
// Same with the submit element, which has the ID "submit"
private WebElement submit;
// But we'd prefer a different name in our code than "comment", so we use the
// FindBy annotation to tell the PageFactory how to locate the element.
@FindBy(name = "comment") private WebElement description;
public EditIssue(WebDriver driver) {
this.driver = driver;
// This call sets the WebElement fields.
PageFactory.initElements(driver, this);
}
@Override
protected void load() {
driver.get("https://github.com/SeleniumHQ/selenium/issues/new");
}
@Override
protected void isLoaded() throws Error {
String url = driver.getCurrentUrl();
assertTrue("Not on the issue entry page: " + url, url.endsWith("/new"));
}
public void setSummary(String issueSummary) {
clearAndType(summary, issueSummary);
}
public void enterDescription(String issueDescription) {
clearAndType(description, issueDescription);
}
public IssueList submit() {
submit.click();
return new IssueList(driver);
}
private void clearAndType(WebElement field, String text) {
field.clear();
field.sendKeys(text);
}
}
それは私たちをあまり信じられなかったようですよね?
これまでに行ったことの1つは、ページに移動する方法に関する情報をページ自体にカプセル化することです。
つまり、この情報はコードベース全体に散らばっていません。
これは、テストで下記を実行できることも意味します。
EditIssue page = new EditIssue(driver).get();
この呼び出しにより、ドライバーは必要に応じてページに移動します。
ネストされたコンポーネント
LoadableComponentsは、他のLoadableComponentsと組み合わせて使用すると、より便利になります。
この例を使用すると、 “edit issue” ページをプロジェクトのWebサイト内のコンポーネントとして表示できます(結局のところ、そのサイトのタブからアクセスします)。
また、issue を報告するにはログインする必要があります。
これをネストされたコンポーネントのツリーとしてモデル化できます。
+ ProjectPage
+---+ SecuredPage
+---+ EditIssue
これはコードではどのように見えますか?
まず、各論理コンポーネントには独自のクラスがあります。
それぞれの “load” メソッドは、親クラスを “get” します。
上記のEditIssueクラスに加えて、最終結果は次のようになります。
ProjectPage.java:
package com.example.webdriver;
import org.openqa.selenium.WebDriver;
import static org.junit.Assert.assertTrue;
public class ProjectPage extends LoadableComponent<ProjectPage> {
private final WebDriver driver;
private final String projectName;
public ProjectPage(WebDriver driver, String projectName) {
this.driver = driver;
this.projectName = projectName;
}
@Override
protected void load() {
driver.get("http://" + projectName + ".googlecode.com/");
}
@Override
protected void isLoaded() throws Error {
String url = driver.getCurrentUrl();
assertTrue(url.contains(projectName));
}
}
and SecuredPage.java:
package com.example.webdriver;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import static org.junit.Assert.fail;
public class SecuredPage extends LoadableComponent<SecuredPage> {
private final WebDriver driver;
private final LoadableComponent<?> parent;
private final String username;
private final String password;
public SecuredPage(WebDriver driver, LoadableComponent<?> parent, String username, String password) {
this.driver = driver;
this.parent = parent;
this.username = username;
this.password = password;
}
@Override
protected void load() {
parent.get();
String originalUrl = driver.getCurrentUrl();
// Sign in
driver.get("https://www.google.com/accounts/ServiceLogin?service=code");
driver.findElement(By.name("Email")).sendKeys(username);
WebElement passwordField = driver.findElement(By.name("Passwd"));
passwordField.sendKeys(password);
passwordField.submit();
// Now return to the original URL
driver.get(originalUrl);
}
@Override
protected void isLoaded() throws Error {
// If you're signed in, you have the option of picking a different login.
// Let's check for the presence of that.
try {
WebElement div = driver.findElement(By.id("multilogin-dropdown"));
} catch (NoSuchElementException e) {
fail("Cannot locate user name link");
}
}
}
EditIssueの “load” メソッドは次のようになります。
@Override
protected void load() {
securedPage.get();
driver.get("https://github.com/SeleniumHQ/selenium/issues/new");
}
これは、コンポーネントがすべて相互に “ネストされている” ことを示しています。
EditIssueで get()
を呼び出すと、そのすべての依存関係も読み込まれます。
使用例:
public class FooTest {
private EditIssue editIssue;
@Before
public void prepareComponents() {
WebDriver driver = new FirefoxDriver();
ProjectPage project = new ProjectPage(driver, "selenium");
SecuredPage securedPage = new SecuredPage(driver, project, "example", "top secret");
editIssue = new EditIssue(driver, securedPage);
}
@Test
public void demonstrateNestedLoadableComponents() {
editIssue.get();
editIssue.setSummary("Summary");
editIssue.enterDescription("This is an example");
}
}
テストで Guiceberry などのライブラリを使用している場合は、PageObjectsの設定の前文を省略して、わかりやすく読みやすいテストを作成できます。
ボットパターン
(以前の場所: https://github.com/SeleniumHQ/selenium/wiki/Bot-Style-Tests)
PageObjectsは、テストでの重複を減らすための便利な方法ですが、チームが快適にフォローできるパターンであるとは限りません。
別のアプローチは、より “コマンドのような” スタイルのテストに従うことです。
“ボット” は、生のSeleniumAPIに対するアクション指向の抽象化です。
つまり、コマンドがアプリに対して正しいことをしていないことがわかった場合、コマンドを簡単に変更できます。
例として:
public class ActionBot {
private final WebDriver driver;
public ActionBot(WebDriver driver) {
this.driver = driver;
}
public void click(By locator) {
driver.findElement(locator).click();
}
public void submit(By locator) {
driver.findElement(locator).submit();
}
/**
* Type something into an input field. WebDriver doesn't normally clear these
* before typing, so this method does that first. It also sends a return key
* to move the focus out of the element.
*/
public void type(By locator, String text) {
WebElement element = driver.findElement(locator);
element.clear();
element.sendKeys(text + "\n");
}
}
これらの抽象化が構築され、テストでの重複が特定されると、ボットの上にPageObjectsを階層化することができます。
2 - テスト自動化について
まず、本当にブラウザを使用する必要があるかどうかを自問することから始めます。
ある時点で複雑な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またはデータベースを介してアカウントを作成し、ユニコーンを事前設定できるようになります。
その後、ユーザーとしてログインし、きらめきを見つけてカートに追加するだけです。
自動化するかしないか
自動化は常に有利ですか?
テストケースの自動化をいつ決定する必要がありますか?
テストケースを自動化することは必ずしも有利ではありません。
手動テストがより適切な場合があります。
たとえば、近い将来にアプリケーションのユーザーインターフェースが大幅に変更される場合は、自動化を書き換える必要があるかもしれません。
また、テストの自動化を構築する時間が足りない場合もあります。
短期的には、手動テストの方が効果的です。
アプリケーションの期限が非常に厳しい場合、現在利用できるテストの自動化はなく、その期間内にテストを実施することが不可欠です。
手動テストが最適なソリューションです。
3 - テストの種類
受け入れテスト
このタイプのテストは、機能またはシステムが顧客の期待と要件を満たしているかどうかを判断するために行われます。
このタイプのテストには通常、顧客の協力またはフィードバックが関与します。
下記質問に答えることで確認することができます。
正しい 製品を作っていますか?
Webアプリケーションの場合、ユーザーの予想される動作をシミュレートすることで、
このテストの自動化をSeleniumで直接実行できます。
このシミュレーションは、このドキュメントで説明されているように、記録/再生によって、
またはサポートされているさまざまな言語によって実行できます。
注:受け入れテストは 機能テスト のサブタイプであり、一部の人はこれにも言及する場合があります。
機能テスト
このタイプのテストは、機能またはシステムが問題なく正常に機能するかどうかを判断するために行われます。
システムをさまざまなレベルでチェックして、すべてのシナリオがカバーされていること、
およびシステムが実行すべきことを実行していることを確認します。
下記質問に答えることで確認することができます。
製品を 正しく 作っていますか?
これは通常以下を含みます。
テストがエラーなし(404、例外…)、使用可能な方法(正しいリダイレクト)で機能する、
利用しやすく、仕様に一致します(上記の 受け入れテスト を参照)。
Webアプリケーションの場合、期待されるリターンをシミュレートすることにより、このテストの自動化をSeleniumで直接実行できます。
このシミュレーションは、このドキュメントで説明されているように、記録/再生またはサポートされているさまざまな言語で実行できます。
パフォーマンステスト
その名前が示すように、パフォーマンステストは、アプリケーションのパフォーマンスを測定するために行われます。
パフォーマンステストには2つの主なサブタイプがあります。
ロードテスト
ロードテストは、定義されたさまざまな負荷(通常、特定の数のユーザーが同時に接続されている場合)でアプリケーションがどの程度機能するかを確認するために行われます。
ストレステスト
ストレステストは、ストレス下(またはサポートされている最大負荷以上)でアプリケーションがどの程度機能するかを確認するために行われます。
一般に、パフォーマンステストは、Seleniumで書かれたテストを実行して、さまざまなユーザーがWebアプリの特定の機能を押して、意味のある測定値を取得することをシミュレートして実行されます。
これは通常、メトリックを取得する他のツールによって行われます。
そのようなツールの1つが JMeter です。
Webアプリケーションの場合、測定する詳細には、スループット、待ち時間、データ損失、個々のコンポーネントの読み込み時間などが含まれます…
注1:すべてのブラウザには、開発者のツールセクションにパフォーマンスタブがあります(F12キーを押すとアクセス可能)
注2:これは一般に機能/機能ごとではなくシステムごとに測定されるため、 非機能テスト のサブタイプです。
回帰テスト
このテストは通常、変更、修正、または機能の追加後に行われます。
変更によって既存の機能が破壊されないようにするために、すでに実行されたいくつかのテストが再度実行されます。
再実行されるテストのセットは、完全または部分的なものにすることができ、アプリケーションおよび開発チームに応じて、いくつかの異なるタイプを含めることができます。
テスト駆動開発 (TDD)
テストタイプそのものではなく、TDDはテストが機能の設計を推進する反復的な開発方法論です。
各サイクルは、機能がパスする単体テストのセットを作成することから始まります(最初に実行すると失敗します)。
この後、テストに合格するための開発が行われます。
別のサイクルを開始してテストが再度実行され、すべてのテストに合格するまでこのプロセスが続行されます。
これは、欠陥が発見されるほどコストが安くなるという事実に基づいて、アプリケーションの開発をスピードアップすることを目的としています。
ビヘイビア駆動開発 (BDD)
BDDは、上記に基づいた反復開発方法論(TDD)でもあり、その目的は、アプリケーションの開発にすべての関係者を関与させることです。
各サイクルは、いくつかの仕様を作成することから始まります(これは失敗するはずです)。
次に、失敗する単体テスト(これも失敗するはずです)を作成し、開発を作成します。
このサイクルは、すべてのタイプのテストに合格するまで繰り返されます。
そのためには、仕様言語が使用されます。
すべての関係者が理解でき、単純で、標準的かつ明示的でなければなりません。
ほとんどのツールは、この言語として Gherkin を使用します。
目標は、潜在的な受入エラーも対象とすることでTDDよりも多くのエラーを検出し、当事者間のコミュニケーションを円滑にすることです。
現在、仕様を記述し、 Cucumber や SpecFlow などのコード関数と一致させるための一連のツールが利用可能です。
Selenium上に一連のツールが構築されており、BDD仕様を実行可能コードに直接変換することにより、このプロセスをさらに高速化しています。 これらのいくつかは、 JBehave、Capybara、およびRobot Framework です。
4 - 推奨された行動
Seleniumプロジェクトからのテストに関するいくつかのガイドラインと推奨事項
「ベストプラクティス」に関するメモ:このドキュメントでは、“ベストプラクティス"というフレーズを意図的に避けています。
すべての状況に有効なアプローチはありません。
“ガイドラインとレコメンデーション"というアイデアを好みます。
これらを一通り読み、特定の環境でどのアプローチが効果的かを慎重に決定することをお勧めします。
機能テストは、多くの理由で適切に行うのが困難です。
まるでアプリケーションの状態、複雑さ、および依存関係が、テストを十分に難しくしないと思えるほど、ブラウザ(特にクロスブラウザの非互換性)を扱うのは、良いテストの作成を難しくします。
Seleniumは、機能的なユーザーインタラクションを簡単にするツールを提供しますが、適切に設計されたテストスイートの作成には役立ちません。
この章では、機能的なWebページの自動化に取り組む方法に関するアドバイス、ガイドライン、および推奨事項を提供します。
この章では、長年にわたって成功を収めてきたSeleniumの多くのユーザーの間で人気のあるソフトウェア設計パターンを記録します。
4.1 - ページオブジェクトモデル
ページオブジェクトは、テストメンテナンスを強化し、コードの重複を減らすためのテスト自動化で一般的になったデザインパターンです。
ページオブジェクトは、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("userName");
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;
if (!driver.getTitle().equals("Sign In Page")) {
throw new IllegalStateException("This is not Sign In Page," +
" current page is: " + driver.getCurrentUrl());
}
}
/**
* 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.2 - ドメイン固有言語(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(テスト対象アプリケーション)
4.3 - アプリケーション状態の生成
Seleniumはテストケースの準備に使用しないでください。
テストケースのすべての反復アクションと準備は、他の方法で行う必要があります。
たとえば、ほとんどのWeb UIには認証があります(ログインフォームなど)。
すべてのテストの前にWebブラウザーからのログインをなくすことで、テストの速度と安定性の両方が向上します。
AUT* にアクセスするためのメソッドを作成する必要があります(APIを使用してログインし、Cookieを設定するなど)。
また、テスト用にデータをプリロードするメソッドの作成は、Seleniumを使用して実行しないほうがいいです。
前述のように、AUT* のデータを作成するには、既存のAPIを活用する必要があります。
*AUT: Application under test(テスト対象アプリケーション)
4.4 - モック外部サービス
外部サービスへの依存を排除すると、テストの速度と安定性が大幅に向上します。
4.5 - 改善されたレポート
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
4.6 - ロケータをうまく扱うTips
どのロケータを指定すべきか、コード内でロケータをどう管理すると良いか。
サポートしているロケータについては 要素を探すを参照してください。
一般に、HTMLのid属性が利用可能でユニークかつ一貫している場合、ページで要素を探す方法として適しています。
idは動作がとても速い傾向があり、複雑なDOMトラバースに伴う処理を省略できます。
ユニークなidが使えない場合、きれいに書かれたCSSセレクタが要素を探す方法として適しています。
XPathはCSSセレクタと同様に動作しますが、シンタックスは複雑で大抵の場合デバッグが困難です。
XPathはとても柔軟ですが、ブラウザベンダは性能テストを通常行っておらず、非常に動作が遅い傾向があります。
link textセレクタとpartial linkText セレクタはa要素でしか動作しないという欠点があります。
加えて、これらはWebDriverの内部でquerySelectorAllの呼び出しに置き換えられます。
タグ名によるロケータは危険な方法になり得ます。
大抵の場合ページ上には同じタグ名の要素が複数あります。タグ名は要素のコレクションを返す findElements(By) メソッドを使う時にもっとも役に立ちます。
ロケータは可能な限り簡潔に、読みやすい状態を保つことを推奨します。
WebDriverでDOM構造のトラバースを行うのは重い処理となります。
検索の範囲を狭めた方がより良い結果を得られます。
4.7 - 状態を共有しない
いくつかの場所で言及されていますが、再度言及する価値があります。
テストが互いに分離されていることを確認してください。
-
テストデータを共有しないでください。
アクションを実行する1つを選択する前に、それぞれが有効な注文をデータベースに照会するいくつかのテストを想像してください。
2つのテストで同じ順序を選択すると、予期しない動作が発生する可能性があります。
-
別のテストで取得される可能性のあるアプリケーション内の古いデータを削除します。 例: 無効な注文レコード
-
テストごとに新しいWebDriverインスタンスを作成します。
これにより、テストの分離が保証され、並列化がより簡単になります。
4.8 - テストの独立性
各テストを独自のユニットとして記述します。
他のテストに依存しない方法でテストを記述してください。
公開後にモジュールとしてWebサイトに表示されるカスタムコンテンツを作成できるコンテンツ管理システム(CMS)があり、CMSとアプリケーション間の同期に時間がかかる場合があるとします。
モジュールをテストする間違った方法は、1つのテストでコンテンツが作成および公開され、別のテストでモジュールをチェックすることです。
コンテンツは公開後、他のテストですぐに利用できない可能性があるため、この方法はふさわしくありません。
代わりに、影響を受けるテスト内でオン/オフできるスタブコンテンツを作成し、それをモジュールの検証に使用できます。
ただし、コンテンツの作成については、別のテストを行うことができます。
4.9 - Fluent APIの使用を検討する
マーチン・ファウラーは“Fluent API”という用語を作り出しました。
Seleniumは既に、FluentWait
クラスでこのようなものを実装しています。
これは、標準のWait
クラスの代替としてのものです。
ページオブジェクトでFluent APIデザインパターンを有効にしてから、次のようなコードスニペットを使用してGoogle検索ページを照会できます。
driver.get( "http://www.google.com/webhp?hl=en&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();
}
}
}
4.10 - テストごとに新しいブラウザを起動する
クリーンな既知の状態から各テストを開始します。
理想的には、テストごとに新しい仮想マシンを起動します。
新しい仮想マシンの起動が実用的でない場合は、少なくともテストごとに新しいWebDriverを起動してください。
Firefoxの場合、既知のプロファイルでWebDriverを起動します。
Most browser drivers like GeckoDriver and ChromeDriver will start with a clean
known state with a new user profile, by default.
WebDriver driver = new FirefoxDriver();
5 - 推奨されない行動
Seleniumでブラウザを自動化するときに避けるべきこと。
5.1 - CAPTCHA(キャプチャ)
CAPTCHA(キャプチャ)は、 Completely Automated Public Turing test
to tell Computers and Humans Apart (コンピューターと人間を区別するための完全に自動化された公開チューリングテスト)の略で、自動化を防ぐように明示的に設計されているため、試さないでください!
CAPTCHAチェックを回避するための2つの主要な戦略があります。
- テスト環境でCAPTCHAを無効にします
- テストがCAPTCHAをバイパスできるようにするフックを追加します
5.2 - ファイルダウンロード
Seleniumの管理下にあるブラウザーでリンクをクリックしてダウンロードを開始することは可能ですが、APIはダウンロードの進行状況を公開しないため、ダウンロードしたファイルのテストには理想的ではありません。
これは、ファイルのダウンロードは、Webプラットフォームとのユーザーインタラクションをエミュレートする重要な側面とは見なされないためです。
代わりに、Selenium(および必要なCookie)を使用してリンクを見つけ、 libcurl などのHTTPリクエストライブラリに渡します。
HtmlUnitドライバーは、
AttachmentHandler インターフェイスを実装することで、
入力ストリームとして添付ファイルにアクセスすることによって、添付ファイルをダウンロードできます。
AttachmentHandlerは、HtmlUnit に追加できます。
5.3 - HTTPレスポンスコード
Selenium RCの一部のブラウザー構成では、Seleniumはブラウザーと自動化されているサイトの間のプロキシとして機能しました。
これは、Seleniumを通過したすべてのブラウザートラフィックをキャプチャまたは操作できることを意味していました。
captureNetworkTraffic()
メソッドは、HTTPレスポンスコードを含むブラウザーと自動化されているサイト間のすべてのネットワークトラフィックをキャプチャすることを目的としています。
Selenium WebDriverは、ブラウザーの自動化に対するまったく異なるアプローチであり、ユーザーのように振る舞うことを好むため、WebDriverを使用してテストを記述する方法で表現します。
自動化された機能テストでは、ステータスコードの確認はテストの失敗の特に重要な詳細ではありません。
それに先行する手順がより重要です。
ブラウザーは常にHTTPステータスコードを表します。たとえば、404または500エラーページを想像してください。
これらのエラーページの1つに遭遇したときに"早く失敗"する簡単な方法は、ページが読み込まれるたびにページタイトルまたは信頼できるポイント(たとえば <h1>
タグ)のコンテンツをチェックすることです。
ページオブジェクトモデルを使用している場合、このチェックをクラスコンストラクターまたはページの読み込みが予想される同様のポイントに含めることができます。
場合によっては、HTTPコードがブラウザーのエラーページに表示されることもあります。
WebDriverを使用してこれを読み取り、デバッグ出力を改善できます。
Webページ自体を確認することは、WebDriverの理想的なプラクティスに沿っており、WebDriverのユーザーのWebサイトの見え方を表現し、主張します。
HTTPステータスコードをキャプチャするための高度なソリューションは、プロキシを使用してSelenium RCの動作を複製することです。
WebDriver APIは、ブラウザーのプロキシを設定する機能を提供します。
Webサーバーとの間で送受信されるリクエストのコンテンツをプログラムで操作できるプロキシがいくつかあります。
プロキシを使用すると、リダイレクトレスポンスコードへの応答方法を決めることができます。
さらに、すべてのブラウザーがWebDriverでレスポンスコードを利用できるようにするわけではないため、プロキシを使用することを選択すると、すべてのブラウザーで機能するソリューションが得られます。
5.4 - Gmail、Eメール、Facebookログイン
複数の理由から、WebDriverを使用してGmailやFacebookなどのサイトにログインすることはお勧めしません。
これらのサイトの使用条件(アカウントがシャットダウンされるリスクがある)に違反することは別として、それは遅く、信頼性がありません。
理想的なプラクティスは、メールプロバイダーが提供するAPIを使用すること、またはFacebookの場合、テストアカウントや友人などを作成するためのAPIを公開する開発者ツールサービスを使用することです。
APIの使用は少し大変な作業のように思えるかもしれませんが、速度、信頼性、および安定性に見返りがあります。
また、APIが変更されることはほとんどありませんが、WebページとHTMLロケーターは頻繁に変更され、テストフレームワークを更新する必要があります。
テストの任意の時点でWebDriverを使用してサードパーティのサイトにログインすると、テストが長くなるため、テストが失敗するリスクが高くなります。
一般的な経験則として、テストが長くなるほど脆弱で信頼性が低くなります。
W3C準拠 のWebDriver実装は、サービス拒否攻撃を軽減できるように、navigator
オブジェクトにWebDriver
プロパティで注釈を付けます。
5.5 - テストの依存関係
自動テストに関する一般的な考え方と誤解は、特定のテスト順序に関するものです。
テストは 任意 の順序で実行でき、成功するために完了するために他のテストに依存してはなりません。
5.6 - パフォーマンステスト
通常、SeleniumとWebDriverを使用したパフォーマンステストはお勧めしません。
それができないからではなく、ジョブに最適化されておらず、良い結果が得られないからです。
ユーザーのコンテキストでパフォーマンステストを行うのが理想的なように思えるかもしれませんが、WebDriverテストスイートは、外部および内部の脆弱性の多くのポイントにさらされます。
たとえば、ブラウザの起動速度、HTTPサーバーの速度、JavaScriptまたはCSSをホストするサードパーティサーバーの応答、およびWebDriver実装自体の計測ペナルティ。 これらのポイントが変わることで、結果が変わります。 Webサイトのパフォーマンスと外部リソースのパフォーマンスの違いを区別することは困難です。また、ブラウザでWebDriverを使用すること、特にスクリプトを挿入する場合のパフォーマンスの低下を把握することも困難です。
他の潜在的な魅力は “時間の節約” です。
機能テストとパフォーマンステストを同時に実行します。
ただし、機能テストとパフォーマンステストには反対の目的があります。
機能をテストするために、テスターは忍耐強くロードを待つ必要があるかもしれませんが、これはパフォーマンステスト結果を曖昧にし、その逆もまた同様です。
Webサイトのパフォーマンスを改善するには、改善すべき点を知るために、環境の違いに関係なく全体的なパフォーマンスを分析し、貧弱なコードプラクティス、個々のリソース(例えば、CSSまたはJavaScript)のパフォーマンスの内訳を特定できる必要があります。
このジョブを実行できるパフォーマンステストツールが既にあり、それらは改善を提案できるレポートと分析を提供します。
使用する(オープンソース)パッケージの例は次のとおりです。: JMeter
5.7 - リンクスパイダー
WebDriverを使用してリンクをスパイダーすることは、実行できないためではなく、最も理想的なツールではないため明らかに推奨される方法ではありません。
WebDriverの起動には時間が必要であり、テストの記述方法によっては、ページに到達してDOMを通過するために数秒から1分かかる場合があります。
このためにWebDriverを使用する代わりに、curl コマンドを実行するか、BeautifulSoupなどのライブラリを使用することにより、これらの方法はブラウザーの作成やページへの移動に依存しないため、時間を大幅に節約できます。
このタスクにWebDriverを使用しないことで、時間を大幅に節約できます。
5.8 - 二要素認証
2FA として知られている2要素認証は、“Google Authenticator” 、
“Microsoft Authenticator” などの"Authenticator" モバイルアプリを使用して、
またはSMS、電子メールで認証することにより、
ワンタイムパスワード(OTP)を生成する認証メカニズムです。
これをシームレスかつ一貫して自動化することは、Seleniumの大きな課題です。
このプロセスを自動化する方法はいくつかあります。
しかし、これはSeleniumテストの上にある別のレイヤーであり、また安全でもありません。
したがって、2FAの自動化を回避したほうがいいです。
2FAチェックを回避するいくつかの選択肢があります。
- テスト環境で特定のユーザーの2FAを無効にして、
それらのユーザー資格情報を自動化で使用できるようにします。
- テスト環境で2FAを無効にします。
- 特定のIPからログインする場合は、2FAを無効にします。
そうすれば、テストマシンのIPを設定してこれを回避できます。