推奨された行動
Seleniumプロジェクトからのテストに関するいくつかのガイドラインと推奨事項
「ベストプラクティス」に関するメモ:このドキュメントでは、“ベストプラクティス"というフレーズを意図的に避けています。
すべての状況に有効なアプローチはありません。
“ガイドラインとレコメンデーション"というアイデアを好みます。
これらを一通り読み、特定の環境でどのアプローチが効果的かを慎重に決定することをお勧めします。
機能テストは、多くの理由で適切に行うのが困難です。
まるでアプリケーションの状態、複雑さ、および依存関係が、テストを十分に難しくしないと思えるほど、ブラウザ(特にクロスブラウザの非互換性)を扱うのは、良いテストの作成を難しくします。
Seleniumは、機能的なユーザーインタラクションを簡単にするツールを提供しますが、適切に設計されたテストスイートの作成には役立ちません。
この章では、機能的なWebページの自動化に取り組む方法に関するアドバイス、ガイドライン、および推奨事項を提供します。
この章では、長年にわたって成功を収めてきたSeleniumの多くのユーザーの間で人気のあるソフトウェア設計パターンを記録します。
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のページに複数のコンポーネントがある場合、コンポーネントごとに個別のページオブジェクトがあると、保守性が向上する場合があります。
また、テストで使用できる他のデザインパターンがあります。
ページファクトリを使用してページオブジェクトをインスタンス化するものもあります。
これらすべてについて議論することは、このユーザーガイドの範囲を超えています。
ここでは、読者にできることのいくつかを認識させるための概念を紹介したいだけです。
前述のように、多くの人がこのトピックについてブログを書いていますし、読者がこれらのトピックに関するブログを検索することをお勧めします。
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(テスト対象アプリケーション)
3 - アプリケーション状態の生成
Seleniumはテストケースの準備に使用しないでください。
テストケースのすべての反復アクションと準備は、他の方法で行う必要があります。
たとえば、ほとんどのWeb UIには認証があります(ログインフォームなど)。
すべてのテストの前にWebブラウザーからのログインをなくすことで、テストの速度と安定性の両方が向上します。
AUT* にアクセスするためのメソッドを作成する必要があります(APIを使用してログインし、Cookieを設定するなど)。
また、テスト用にデータをプリロードするメソッドの作成は、Seleniumを使用して実行しないほうがいいです。
前述のように、AUT* のデータを作成するには、既存のAPIを活用する必要があります。
*AUT: Application under test(テスト対象アプリケーション)
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
6 - ロケータをうまく扱うTips
どのロケータを指定すべきか、コード内でロケータをどう管理すると良いか。
サポートしているロケータについては 要素を探すを参照してください。
一般に、HTMLのid属性が利用可能でユニークかつ一貫している場合、ページで要素を探す方法として適しています。
idは動作がとても速い傾向があり、複雑なDOMトラバースに伴う処理を省略できます。
ユニークなidが使えない場合、きれいに書かれたCSSセレクタが要素を探す方法として適しています。
XPathはCSSセレクタと同様に動作しますが、シンタックスは複雑で大抵の場合デバッグが困難です。
XPathはとても柔軟ですが、ブラウザベンダは性能テストを通常行っておらず、非常に動作が遅い傾向があります。
link textセレクタとpartial linkText セレクタはa要素でしか動作しないという欠点があります。
加えて、これらはWebDriverの内部でquerySelectorAllの呼び出しに置き換えられます。
タグ名によるロケータは危険な方法になり得ます。
大抵の場合ページ上には同じタグ名の要素が複数あります。タグ名は要素のコレクションを返す findElements(By) メソッドを使う時にもっとも役に立ちます。
ロケータは可能な限り簡潔に、読みやすい状態を保つことを推奨します。
WebDriverでDOM構造のトラバースを行うのは重い処理となります。
検索の範囲を狭めた方がより良い結果を得られます。
7 - 状態を共有しない
いくつかの場所で言及されていますが、再度言及する価値があります。
テストが互いに分離されていることを確認してください。
-
テストデータを共有しないでください。
アクションを実行する1つを選択する前に、それぞれが有効な注文をデータベースに照会するいくつかのテストを想像してください。
2つのテストで同じ順序を選択すると、予期しない動作が発生する可能性があります。
-
別のテストで取得される可能性のあるアプリケーション内の古いデータを削除します。 例: 無効な注文レコード
-
テストごとに新しいWebDriverインスタンスを作成します。
これにより、テストの分離が保証され、並列化がより簡単になります。
8 - テストの独立性
各テストを独自のユニットとして記述します。
他のテストに依存しない方法でテストを記述してください。
公開後にモジュールとしてWebサイトに表示されるカスタムコンテンツを作成できるコンテンツ管理システム(CMS)があり、CMSとアプリケーション間の同期に時間がかかる場合があるとします。
モジュールをテストする間違った方法は、1つのテストでコンテンツが作成および公開され、別のテストでモジュールをチェックすることです。
コンテンツは公開後、他のテストですぐに利用できない可能性があるため、この方法はふさわしくありません。
代わりに、影響を受けるテスト内でオン/オフできるスタブコンテンツを作成し、それをモジュールの検証に使用できます。
ただし、コンテンツの作成については、別のテストを行うことができます。
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();
}
}
}
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();