ENGINEER BLOG

ENGINEER BLOG

Excel で作った Web アプリケーションのテスト仕様書を Selenium で自動実行する

こんにちは。インテグレーションサービス本部の横山です。
今回は、開発業務寄りの話題をご紹介しようと思います。

業務アプリケーションのテスト工程では、一般的にまずテスト仕様書を作成し、それに沿って画面を操作して動作確認を行ないます。 Seleniumを使用すると、画面操作やキャプチャを自動化できますが、そのためにはSeleniumに対するコマンドを埋め込んだテストクラスを作成する必要があります。

つまり、作業の流れが、

テスト仕様書作成→テスト実行(手動)
から、
テスト仕様書作成→テストクラス作成→テスト実行(自動)
となるため、テスト実行を何度も繰り返さないと楽をした気になれません。もちろん回帰テストの際には威力を発揮するはずですが、テストクラスに埋め込まれたテスト実装の可読性や、テスト仕様書と同期をとる必要性といった問題もあり、開発現場での適用が必ずしも成功していませんでした。 そこで今回、テストクラスを書くのではなく(*)、テスト仕様書を直接実行できるツールを作成してみました。
* ツールを起動するためのテストクラスは必要になります。

使うもの


  • Selenium Web Driver(Selenium2)
  • Apache POI

概要


  • Excelで作成したテスト仕様書を読み込み、テストを実行する。
  • テスト結果をテスト仕様書に書き込み、別ファイルに出力する。

テスト仕様書


こんな感じで作成します。
001
この例では、「パスワード変更画面」で自分のパスワードを変更するケースをテストしています。
操作の流れは、

  • №1~6.ブラウザを開いてWebアプリケーションにアクセスし、ユーザID、パスワードを入力してログインする。
  • №7~9.メニューから「パスワード変更」を選ぶ。
  • №10~16.現在のパスワードと新しいパスワードを入力して「更新」ボタンをクリックする。
  • №17~21.確認ダイアログが表示されるので「OK」をクリックする。処理完了メッセージを確認する。
  • №22~25.Webアプリケーションからログアウトし、ブラウザを閉じる。
のようになっています。

必須なのはD~FとH列で、D~F列にコマンド(@COMMAND)、操作対象(@TARGET)、投入する値(@VALUE)を書きます。実行結果(@RESULT)はH列に書き込まれます。
その他の列は自由に使用することができます。

コマンド


以下のコマンドが使用できます。

  • open …… ブラウザを開き、指定されたURLにアクセスします。@VALUEにウィンドウサイズを指定できます。
  • close …… ブラウザを閉じます。
  • wait …… 指定の状態になるまで待ちます。例では、カーソルが通常の状態(砂時計でない)になるまで待ちます。
  • set …… 指定した画面要素に値を設定します。画面要素の指定は、id属性のほか、name属性、タグ種別(inputなど)、cssクラス名、XPathなどが使用できます。
  • get …… 指定した画面要素の値を取得します。取得した値は@RESULT列に書き込まれます。
  • click …… 指定した画面要素をクリックします。
  • capture …… 現在の画面をキャプチャします。@TARGETに指定されたファイルにPNGファイルとして出力します。

テスト結果


上記の例を実行すると、このようなファイルが出力されます。
002
I列(判定)には、期待値と、setで取得した値が等しい場合に「OK」と表示する式が書いてあります。
@RESULT列には、setコマンドでは取得した値を、それ以外の場合、コマンドが成功したかを出力します。「DONE」になっていれば成功です。
@RESULTにエラーの行がなく、判定がすべて「OK」になっていればテスト完了です。

Selenium2の使い方


一般的な使用法はWeb上にも情報がたくさんありますので、ここでは、このツールの機能を実現するのに使用した実装の一部をご紹介します。

open

Selenium2では、ブラウザ製品ごとのWebDriverというクラスが必要です。これはexeファイルとして提供されていますので、その格納場所をシステムプロパティに設定する必要があります。これをツール側に実装すると使い勝手が悪くなるので、ツールの呼び出し元(JUnitのテストクラス)で行ないます。

// IE用ドライバのパスを格納する。
System.setProperty(InternetExplorerDriverService.IE_DRIVER_EXE_PROPERTY,
                   new File("driver/IEDriverServer32.exe").getCanonicalPath());
// Chrome用ドライバのパスを格納する。
System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY,
                   new File("driver/chromedriver.exe").getCanonicalPath());

ツール側では、対象のブラウザに合わせたWebDriverのインスタンスを生成します。

// WEBドライバを生成する。
switch (this.browserType) {
  case INTERNET_EXPLORER:
    this.driver = new InternetExplorerDriver();
    break;
  case GOOGLE_CHROME:
    this.driver = new ChromeDriver();
    break;
  case MOZILLA_FIREFOX:
    this.driver = new FirefoxDriver();
    break;
  default:
    break;
}

生成したら、ウィンドウサイズを設定します。

this.driver.manage().window().setSize(new Dimension(this.windowWidth,
                                                    this.windowHeight));

指定されたURLを開きます。

this.driver.get(url);

close

WebDriverを閉じます。

this.driver.close();
this.driver.quit();

wait

カーソルの状態を待つ実装は、次のようになります。

try {
  final Object testCursor = value;
  WebDriverWait wait =
    new WebDriverWait(driver, this.getWaitLimitMSec(),
                      this.getWaitIntervalMSec());
  Boolean waitRet = wait.until(new ExpectedCondition() {
    @Override
    public Boolean apply(WebDriver arg0) {
      Object cursor =
        StandardSeleniumDriver.this.execJavaScript("return document.body.style.cursor;");
      boolean ret = objectEquals(cursor, testCursor);
      return ret;
    }
  });
  if (waitRet != null) {
    ret = waitRet;
  }
}
catch (Exception e) {
  :
}

set

まず、@TARGETの値から対象となる画面要素を特定します。このツールでは、id属性以外を指定するときは、@TARGETに接頭辞を付ける仕様にしています。例えばname属性を指定するときは「NAME:userId」のようにします。 該当する画面要素(WebElement)のリストを取得します。 「TARGET_」で始まる定数は、接頭辞の値です。

List ret;
String upcaseTarget = target.toUpperCase(); // キーワード検出用のため、大文字で揃える。
WebDriver drv = this.getDriver();
// id属性が指定されたとき
if (upcaseTarget.startsWith(TARGET_ID)) {
  ret = drv.findElements(By.id(this.removePrefix(target, TARGET_ID)));
}
// name属性が指定されたとき
else if (upcaseTarget.startsWith(TARGET_NAME)) {
  ret = drv.findElements(By.name(this.removePrefix(target, TARGET_NAME)));
}
// タグ名が指定されたとき
else if (upcaseTarget.startsWith(TARGET_TAG_NAME)) {
  ret = drv.findElements(By.tagName(this.removePrefix(target,
                                                      TARGET_TAG_NAME)));
}
// class属性が指定されたとき
else if (upcaseTarget.startsWith(TARGET_CLASS)) {
  ret = drv.findElements(By.className(this.removePrefix(target,
                                                        TARGET_CLASS)));
}
// CSSセレクタが指定されたとき
else if (upcaseTarget.startsWith(TARGET_CSS_SELECTOR)) {
  ret = drv.findElements(By.cssSelector(this.removePrefix(target,
                                                          TARGET_CSS_SELECTOR)));
}
// XPathが指定されたとき
else if (upcaseTarget.startsWith(TARGET_XPATH)) {
  ret = drv.findElements(By.xpath(this.removePrefix(target, TARGET_XPATH)));
}
// 上記以外のとき(id属性と仮定する)
else {
  ret = drv.findElements(By.id(target));
}

該当する画面要素が複数存在するときは、最初の要素に値を設定します。 値の設定方法は、画面要素の種類によって異なります。 通常の入力域はキー入力を実行するWebElement#sendKeysを使用しますが、チェックボックス、ラジオボタン、選択コントロールは、JavaScriptを使用して値の設定を行なうようにしました。

String tagName = element.getTagName();
String valStr = (value == null ? "" : value.toString());
// 入力タグのとき
if (TAG_INPUT.equalsIgnoreCase(tagName)) {
  String type = element.getAttribute("type");
  // 1行入力域のとき
  if (TYPE_TEXT.equalsIgnoreCase(type)) {
    element.clear();
    element.sendKeys(valStr);
  }
  // パスワード入力域のとき
  else if (TYPE_PASSWORD.equalsIgnoreCase(type)) {
    element.clear();
    element.sendKeys(valStr);
  }
  // チェックボックスのとき
  else if (TYPE_CHECKBOX.equalsIgnoreCase(type)) {
    this.execJavaScript("document.getElementById('" +
                        element.getAttribute("id") + "').checked = " +
                        Utils.strToBool(valStr) + ";");
  }
  // ラジオボタンのとき
  else if (TYPE_RADIO.equalsIgnoreCase(type)) {
    this.execJavaScript("document.getElementById('" +
                        element.getAttribute("id") + "').checked = " +
                        Utils.strToBool(valStr) + ";");
  }
}
// 複数行入力域のとき
else if (TAG_TEXTAREA.equalsIgnoreCase(tagName)) {
  element.clear();
  element.sendKeys(valStr);
}
// 選択タグのとき
else if (TAG_SELECT.equalsIgnoreCase(tagName)) {
  this.execJavaScript("document.getElementById('" +
                      element.getAttribute("id") +
                      "').value = '" + valStr + "';");
}
// 上記以外のとき
else {
  element.clear();
  element.sendKeys(valStr);
}

最後のelseの処理が危ないなと思う方もいらっしゃると思います。テストツールなので、フェイルセーフよりは「とにかく動く」ことを優先しました。そのため、不明な要素であってもとりあえずキー入力で値を設定します。安全側に倒すなら、ここは例外を投げるなどするところです。

get

対象の画面要素を特定する部分はsetと同様です。画面要素の値を取得するには、inputタグやselectタグはvalue属性を取得します。その他のタグではtextを取得します。

String tagName = ele.getTagName();
if (TAG_INPUT.equalsIgnoreCase(tagName)) {
  ret = Utils.blankWhenNull(ele.getAttribute("value"));
}
else if (TAG_SELECT.equalsIgnoreCase(tagName)) {
  ret = Utils.blankWhenNull(ele.getAttribute("value"));
}
else {
  ret = Utils.blankWhenNull(ele.getText());
}

click

対象の画面要素を特定する部分はsetと同様です。

ele.click();

capture

画面のキャプチャを取得するにはTakesScreenshot#getScreenshotAsを使用します。
ファイルがTempフォルダに出力されるので、指定されたファイル名にコピーします。

File file = ((TakesScreenshot)this.getDriver()).getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(file, new File(filename));

ダイアログボックスの扱い

ダイアログボックスについては、画面要素とは異なる扱いが必要です。 まず、ダイアログボックスが表示されるのを待ち、表示されたら取得します。

Alert ret = null;
try {
  WebDriverWait wait = new WebDriverWait(this.getDriver(), WAIT_SEC_FOR_DIALOG);
  ret = wait.until(ExpectedConditions.alertIsPresent());
}
catch (Exception e) {
  :
}

ダイアログボックスのテクストを取得するときは、Alert#getTextを実行します。

// ダイアログボックスを取得する。
Alert dialog = this.getDialog();
if (dialog != null) {
  // メッセージを取得するとき
  if ("text".equalsIgnoreCase(label)) {
    ret = dialog.getText();
  }
}

ダイアログボックスのボタンをクリックするときも、Alertのメソッドを実行します。

// ダイアログボックスを取得する。
Alert dialog = this.getDialog();
if (dialog != null) {
  // OKボタンをクリックするとき
  if ("ok".equalsIgnoreCase(btn)) {
    dialog.accept();
    ret = true;
  }
  // キャンセルボタンをクリックするとき
  else if ("cancel".equalsIgnoreCase(btn)) {
    dialog.dismiss();
    ret = true;
  }
  // 上記以外のとき
  else {
    :
  }
}

以上、Selenium Web Driverを使ったテスト自動化ツールのあらましでした。


実際の業務では、画面操作だけでテストが実行できるケースは限られていて、テスト実行前にデータベースにデータを投入したり、実行後にデータを取得して確認するといった作業が必要です。
Seleniumによる自動化と併せて、これらの操作を行なうツールも作成したのですが、それについては別の機会にご紹介したいと思います。