More Related Content Similar to Wagby Testing Framework Similar to Wagby Testing Framework (20) More from Yoshinori Nie (19) Wagby Testing Framework16. Selenium
• Selenium (http://www.seleniumhq.org)
• ブラウザテストの世界標準
• W3Cで勧告済み (WebDriver)
• マルチププログラミング言語(Java,Ruby,php,JavaScript等)
• マルチプラットフォーム(Windows,Mac,Linux,モバイルOS)
• クロスブラウザ(IE,Edge,Google Chrome,Firefox,Safari)
16/97
17. Selenium の問題点
• 複雑なことをするとコードの記述量が増える
• テストの安定性に欠ける
• テストコードが速く動きすぎて描画が追いつかずエラー
• JavaScriptを使って描画を行う(非同期)アプリケーションでは特に顕著
• Implicit Wait(暗黙的な待機)機能では対応できないケースが多い
• 解決策 : テストコードに wait を明示
17/97
18. Selenide
• Selenide (http://selenide.org)
• Seleniumを拡張したJavaテスティングフレームワーク
• Selenium: ブラウザを操作することを目的(Low-Level API)
• Selenide : ブラウザテストに特化
• 少ない記述量で実装可能
• 非同期操作のサポート(waitの記述が不要)
• デフォルトで4秒waitするようになっている
• 4秒間100ミリ秒間隔でリトライする(変更可能)
• 描画が行われれば4秒待たずにテストが実行される
• 異常時にスクリーンショットとhtmlが自動保存される
18/97
19. サンプルコード(Selenide)
• jQueryライクな記述
• CSSセレクタで要素を指定して、処理を行う
public static void main0(String[] args) {
open("https://wagby.com/event/wdd20XX/form.jsp");
$("input[name=¥"organization¥"]").val("ジャスミンソフト");
$("input[name=¥"name¥"]").val("ジャスミン太郎");
$("input[name=¥"dept¥"]").val("営業部");
$("input[name=¥"title¥"]").val("なし");
$("input[name=¥"tel¥"]").val("000-111-2222");
$("input[name=¥"mailaddress¥"]").val("taro@example.com");
$("input[name=¥"agreement¥"]").click();
$("#apply").click();
}
19/97
20. waitの記述
• Selenium
• Selenide
WebDriverWait wait = new WebDriverWait(driver, 4000); //待ち時間を指定
By button = By.id("apply");
wait.until(
ExpectedConditions.visibilityOfElementLocated(button));
driver.findElement(button).click();
$("#apply").click(); // waitの記述は不要
20/97
23. Wagby側での工夫
• 各項目、ボタンにはidを割り当てる
• HTMLが複雑でもidを指定することでテストコードをシンプルにすることがで
きる
• idはユニーク性が保証されているのでテストミスが発生しない
<div class="action_button">
<span class="..." role="presentation" widgetid="btnSend">
<span class="..." data-dojo-attach-event="ondijitclick:__onClick" role="presentation">
<span class="..." role="button" id="btnSend" style="user-select: none;">
<span class="..." data-dojo-attach-point="iconNode"></span>
<span class="...">●</span>
<span class=“...” id=“btnSend_label”>保存</span>
</span>
</span>
<input name="btnSend" type="button" value="" class="...">
</span>
</div>
保存ボタンのHTML
23/97
24. 入力フィールド
• 通常の入力フィールド、テキストエリア
• 入力フィールドのid属性の構造
• idに含まれる「$」記号はバックスラッシュでエスケープ
(CSSセレクタのルール。jQueryも同じ)
$("#customer_p¥¥$002fname").val("ジャスミン太郎");
<input type="text" id="customer_p$002fname" name="customer_p$002fname" ..>
customer_p¥¥$002fname
モデル名+「_p」
「$」のエスケープ文字 モデル名と項目名の区切り文字
項目名
24/97
26. パターン化されたHTML
• 複雑なHTMLの中にもパターンがある
• idの命名規則
• 入力フィールド: cucstomer_p¥¥$002fname
• 時間リストボックスの「時」: customer_p¥¥$002fstart_hour
• 時間リストボックスの「分」: customer_p¥¥$002fstart_minute
• リストボックスの操作パターン
• STEP1:▼をクリックしてプルダウンを表示
• STEP2:プルダウンから時間を選ぶ
26/97
27. Wagby Testing Framework(WTF)
• Wagbyに特化したTesting Frameworkを提供
• パターン化された共通部分は記述不要に
• 編集画面での入力もシンプルに
• 長いIDの記述は不要
• 通常項目は、項目名と値のみを指定
• 各種ボタン操作のためのメソッドを用意
• ボタンに付与されたidを気にする必要はない
• 提携処理を一つのメソッドで
• ログオン画面にアクセス > ユーザー名、パスワードの入力 > ログオンボタンをクリック
• 「キャンセル」ボタンをクリック > 確認ダイアログで「OK」をクリック
27/97
28. テストの事前準備
• ChromeDriver(WebDriver for Chrome)ファイルの取得
• https://sites.google.com/a/chromium.org/chromedriver/downloads
• ChromeDriverファイルの管理方法を決める
• 各開発者環境のブラウザバージョンが統一されていない場合
• ChromeDriverファイルは開発者ごとに個別管理
• 各開発者環境のブラウザバージョンが統一されている場合
• ChromeDriverファイルはバージョン管理システムで統一管理(custoizeフォルダ内に配置)
• SelenideにはChromeDriverの自動取得機能があるが…
• 常に最新のChromeDriverを自動取得する
• 最新のChromeDriverの利用にはSelenideのバージョンアップが必要なケースも
あった
• WagbyR8.3.0 以前のバージョンは最新のChromeDriverが動作しない
• 古いChromeDriver(2.38)を利用すればテストは実施可能
• SelenideのバージョンアップにはWagbyのアップデートが必要
• これが原因で自動テストが失敗してしまうことも
• 安定したテストの実施には固定のWebDriverを利用するのが良い
28/97
30. テストクラスの作成
import static com.codeborne.selenide.Condition.*;
import static com.codeborne.selenide.Selenide.*;
import static jp.jasminesoft.jfc.test.support.selenide.Operations.*;
import static jp.jasminesoft.util.ExcelFunction.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
...
public class MyFirstTest {
@BeforeClass
public static void setUpBeforeClass() {
// テストの前処理を記述
Configuration.baseUrl = "http://localhost:8921/wagby"; // テスト対象システムのURL
Configuration.browser = WebDriverRunner.CHROME; // 利用するブラウザ
// WebDriver ファイルの指定
System.setProperty("webdriver.chrome.driver","customize/chromedriver.exe");
}
@AfterClass
public static void tearDownAfterClass() {
// テストの後処理を記述
}
...
}
30/97
31. シンプルなテスト
• ログオンしてメニューが表示されることを確認するだけのテスト
public class MyFirstTest {
...
@Test
public void test() {
// ログオン
logon("admin", "wagby");
// メニュー画面が表示されていることを確認
pageTitle().shouldHave(exactText("メニュー"));
// ログオフ
logoff();
// ログオン画面が表示されていることを確認
assertThat(title(), is("Wagby アプリケーション ログオン"));
}
}
31/97
32. テストの実行と結果の確認
• Eclipseでの実行
• メニューから「実行 > 実行(S) > JUnitテスト」を選んでテスト実行
• JUnitビューに結果が表示されます
• Antでの実行
1. コマンドプロンプトで misc フォルダへ移動
2. 以下のコマンドで、Ant を使ってテストクラスのコンパイル、Tomcatの起動、
テストの実行、結果ファイルの生成、Tomcatの停止を実行
3. 実行結果はwork/target/junit/index.htmlファイルを確認
• JUnitのレポート形式での確認ができる
• テスト実装中はEclipse、定期テストの実行はAntを使うのがオススメ
libant wtf.clean wtf.build tomcat-start wtf.test wtf.junitreport tomcat-stop
※Tomcatが起動済みの場合は tomcat-start, tomcat-stop を除去して実行
32/97
33. Antでの実行(詳細)
• Antの各ターゲットの処理内容
• 各種設定はmisc/build.propertiesに記述可能
ターゲット名 処理内容
wtf.clean テストクラスやjunitレポートを削除します。
wtf.build customize/test 以下に配置されたテストクラスをコンパイルします。
tomcat-start tomcat を起動します(wagbyapp/bin に移動して startup.bat を実行)
wtf.test junit テストを実行します。
wtf.junitreport junit レポートを作成します。
tomcat-stop tomcatを停止します(wagbyapp/bin に移動して shutdown.bat を実行)
# Tomcatの起動待ち時間(デフォルトは180秒)
tomcat.start.sleep.seconds=120
# junitテストクラス(デフォルトはjp/jasminesoft/wagby/tests/AllTests.java)※ant のパターン表記利用可。
suiteClasses=jp/jasminesoft/wagby/**/*Test*.java
# junitレポートファイルの出力フォルダ
junit.output.dir=c:¥¥tmp¥¥report
33/97
35. ログオン(selenide)
• ユーザー名とパスワードを入力してログオン
// http://localhost:8921/wagby/ へアクセス
open("/");
// ユーザー名とパスワードを入力後、ログオン
$("#user").val("admin");
$("#pass").val("wagby").submit();
<html><head>
<title>Wagby アプリケーション ログオン</title>
</head><body>
<form ...>
<input id="user" name="user" type="text">
<input id="pass" name="pass" type="password">
</form>
</body></html>
ログオンフォームログオン画面の簡易HTML
35/97
37. ログオン(WTF)
• 共通処理を行うOperationsクラスを用意
• static インポートで更に省略
// ログオン処理を行う。
Operations.logon("admin", "wagby");
// ページタイトルを確認
Operations.pageTitle().shouldHave(exactText("メニュー"));
import static com.codeborne.selenide.Condition.*;
import static jp.jasminesoft.jfc.test.support.selenide.Operations.*;
…
// ログオン処理を行う。
logon("admin", "wagby");
// ページタイトルを確認
pageTitle().shouldHave(exactText("メニュー"));
37/97
38. 操作と確認
• テストは操作と確認の繰り返し
• ログオン操作 : logon("admin", "wagby")
• 操作後のページタイトルの確認 :
pageTitle().shouldHave(exactText("メニュー"))
• データの入力 → 確認
• 画面遷移ボタンのクリック → 遷移後の画面名の確認
• 画面遷移操作後は必ずページタイトルを確認する
38/97
40. エラーチェック(WTF)
• Operations#checkNoErrors() メソッド
• 画面上にエラーメッセージが無いこと及びJavaScriptのエラーが発生してい
ないことを確認。
• checkNoErrors() の自動呼び出し(暗黙的な確認)
• Operations クラスが提供する操作用メソッドでは、操作後のエラーメッセー
ジ確認のため、checkNoErrors()が自動的に呼び出される。
// エラーが無いことを確認します。
checkNoErrors();
// ログオン処理を行う(内部でcheckNoErros()を自動実行)。
logon("admin", "admin");
// checkNoErrors(); // 個別実行は不要。
40/97
41. 異常系のテスト(WTF)
• エラーメッセージの内容確認
• 例)初期パスワードでのログオン
• logon()メソッドの第三引数にfalseを指定す
ることでcheckNoErrors()を呼び出さない
• エラー内容をチェックするコードをテストと
して実装する
import static com.codeborne.selenide.CollectionCondition.*;
…
// ログオン処理を行う(checkNoErros()は行わない)。
logon("user01", "user01", false);
// エラーメッセージの完全一致チェック。
errors().shouldHave(exactTexts("初期パスワードがセットされていま
す。パスワードの変更を行ってください。パスワードを変更するまでは、操作が
制限される可能性があります。"));
41/97
42. 異常系のテスト(WTF)
• 複数のエラーメッセージのチェック
import static com.codeborne.selenide.CollectionCondition.*;
…
// 複数エラーメッセージの完全一致チェック。
errors().shouldHave(exactTexts(
"エラーメッセージ01",
"エラーメッセージ02",
"エラーメッセージ03"));
// 複数エラーメッセージの部分一致チェック。
errors().shouldHave(texts(
"メッセージ01",
"メッセージ02",
"メッセージ03"));
42/97
45. メニュー(WTF)
メインメニュー サブメニュー
// メインメニューでの画面遷移
// 「サービス」タブを選択後、「顧客検索」メニューをクリック
selectMenu("サービス", "顧客検索");
// ページタイトルを確認
pageTitle().shouldHave(exactText("顧客 検索"));
// サブメニューでの画面遷移
// 大項目「サービス」を選択後、プルダウンから「顧客検索」を選択する
selectSubMenu("サービス", "顧客検索");
pageTitle().shouldHave(exactText("顧客 検索"));
45/97
47. 保存/キャンセル処理
• 保存処理
• キャンセル処理
// 「保存」ボタンをクリック
save();
// ページタイトルを確認
pageTitle().shouldHave(exactText("顧客 詳細表示"));
保存/キャンセルボタン
//「キャンセル」ボタンをクリック。クリック後に表示される確認ダイアログも
//自動的にOKをクリックします。
cancel();
// ページタイトルを確認
pageTitle().shouldHave(exactText("顧客 詳細表示"));
47/97
49. Operationsクラスが提供するメソッド
• 登録・更新・コピー登録画面用
• 詳細・検索・一覧画面用
メソッド名 処理内容
clickNewButton() 「登録画面へ」ボタンをクリックする。
clickEditButton() 「更新画面へ」ボタンをクリックする。
clickDeleteButton() 「削除」ボタンをクリックする。
clickCopyButton() 「コピー登録へ」ボタンをクリックする。
clickDetailButton() 外部キー親モデルの詳細表示画面へ遷移するためのボタンを
クリックする。
clickSearchPageButton() 「検索画面へ」ボタンをクリックする。
clickListPageButton() 「一覧画面へ」ボタンをクリックする。
search() 検索画面の「検索の実行」ボタンをクリックする。
resetSearch() 検索画面の「リセット」ボタンをクリックします。
clickUpdateListPageButton() 「一覧更新へ」ボタンをクリックする。
メソッド名 処理内容
save() 「保存」ボタンをクリックする。
cancel() 「キャンセル」ボタンをクリックする。
49/97
50. Operationsクラスが提供するメソッド
• メニュー
• グローバルリンク
メソッド名 処理内容
selectMenu(“タブ名”, “メニュー名”) 指定のメニューを選択する。
selectSumMenu(“グループ名”, “メニュー名”) 指定のメニューを選択する。
メソッド名 処理内容
menu() 「メニューへ戻る」をクリックする。
logoff() 「ログオフ」をクリックする。
changeFontSize(size)
* sizeはlarge,medium,smallを指定
「XXXフォントサイズ」をクリックする。
50/97
52. Operationsクラスが提供するメソッド
• ログオン画面
• 全画面共通
clickButton(“ボタン表示名”) 指定のボタンをクリックする。
getButton(“ボタン表示名”) 指定のボタンを取得する。
pageTitle() ページタイトルを取得する。
infos() infoメッセージを取得する。
warns() warnメッセージを取得する。
errors() errorメッセージを取得する。
checkSuccessMessages() 正常終了メッセージが表示されていることを確認する。
※ただし、このメソッドはsave()メソッド呼び出し時に自動実行さ
れるため、通常は明示的に呼び出す必要はない。
confirmIfExists() 確認ダイアログが表示されている場合、OK ボタンをク
リックします。
changeWindowSize(${width}, ${height}) ブラウザのウィンドウサイズを変更します。
メソッド名 処理内容
logon(${userid}, ${passwd}, ${言語}) ログオン画面を表示し、ログオン処理を行います
(第三引数の${言語}は省略可)
selectLanguage(${言語}) 言語を選択します。
52/97
53. 入力フィールド
• 項目への入力 : val() メソッド
• 内部では次のようなidを組み立てて動作している。
• 入力値の確認 : shouldHave() メソッド
// モデルの情報を保持するインスタンス
WebModel model = new WebModel("customer");
// customerモデルのname項目への入力
new WebModelitem<>(model, "name").val("ジャスミン太郎");
// 「ジャスミン太郎」と入力されていることを確認
new WebModelitem<>(model, "name").shouldHave("ジャスミン太郎");
$("#customer_p¥¥$002fname").val("ジャスミン太郎");
53/97
54. リストボックス
• ListBoxクラスを利用
• 通常項目と同様に val() メソッドでも
値のセットは可能。
// リストボックス:「地方公共団体」を選択する
new ListBox(model, "customertype")
.dropdown()
.select("地方公共団体");
// こちらでも可(内部処理は同じ)
new ListBox(model, "customertype")
.val("地方公共団体");
リストボックス
54/97
58. 日付/時間型リストボックス
• DateListBox/TimeListBoxクラスを利用
// 時間型リストボックス
TimeListBox start = new TimeListBox(model, "start");
//「時」入力用のListBoxを取得します。
ListBox startHour = start.hourListBox();
// 15時をセット
startHour.dropdown().select(15);
//「分」入力用のListBoxを取得します。
ListBox startMinute = start.minuteListBox();
// 13分をセット
startMinute.val(30);
// こちらでも可(内部処理は同じ)
new TimeListBox(model, "start").val("15:30");
時間型リストボックス
58/97
59. 繰返し項目
// 繰り返し項目「email」を操作するオブジェクトを作成。
new WebMultiModelitem<>(model, "email")
//.clear() // 既存の入力値全てを削除
.get(1) // 1番目のテキストフィールドを取得
.val("taro@jasminesoft.co.jp")
.add() // 「追加」ボタンをクリック
.val("sales@jasminesoft.co.jp");
// こちらでも可(既存の入力は全て削除してから、値をセットする)
new WebMultiModelitem<>(model, "email")
.val("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
繰返し項目
59/97
60. さまざまな種類の項目への対応
• 全て val() で入力、shouldHave()で確認可能
項目の種類 対応するJavaクラス
入力フィールド WebModelitem
リストボックス ListBox
ラジオボタン RadioButton
チェックボックス CheckBox
検索画面(サブウィンドウ) SearchList
日付入力フィールド(DatePicker) DateTextBox
日付リストボックス DateListBox
時間リストボックス TimeListBox
追記型リストボックス ComboBox
郵便番号 Postcode
繰返し項目 WebMultiModelitem
60/97
61. 各項目への値の入力
// 通常項目
new WebModelitem<>(model, "name").val("ジャスミン太郎");
// リストボックス
new ListBox(model, "customertype").val("地方公共団体");
// チェックボックス
new CheckBox(model, "customertype").val("民間企業", "地方公共団体");
// 他モデルの参照(検索画面)
new SearchList(model, "account").val("admin");
// 日付
new DateTextBox(report01, "startdate").val("2017-07-01");
// 時間型リストボックス
new TimeListBox(model, "start").val("15:30");
// 繰返し項目
new WebMultiModelitem<>(model, "email")
.val("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
61/97
64. 日付項目
• 直接入力のみ行う場合はWebModelitemでよい
• DatePicker を使って入力テストを行う場合はDateTextBoxクラスを利
用
new WebModelitem<>(model, "startdate").val(“2017-01-01");
// DatePickerを操作するオブジェクトを取得する
DatePicker datePicker
= new DateTextBox(model, "startdate").datePicker();
datePicker.nextMonth(); // 翌月へ
datePicker.prevMonth(); // 前月へ
datePicker.nextYear(); // 翌年へ
datePicker.prevYear(); // 前年へ
datePicker.selectDate(1); // 1日を選択
//datePicker.selectCell(2,3); // 2行、3列目のセルを選択
64/97
65. 追記型リストボックス
• ComboBoxクラスを利用
// 追記型リストボックス
ComboBox companyname
= new ComboBox(model, "companyname");
// 直接入力を行う場合(存在しない選択肢も可)
companyname.val("ジャスミンソフト");
// ドロップダウンから選択する場合
// 存在しない選択肢を選んだ場合はエラー
companyname
.dropdown()
.select("ジャスミンソフト");
追記型リストボックス
65/97
66. ファイル型項目(new R8.4.2)
• ファイルのアップロード
• ファイル名の確認、ファイルのダウンロード
// ファイルのアップロード。
// アップロードするファイルは $(DEVHOME)に配置。
new FileType(model, "avatarImage").uploadFile(new File("a.png"));
// こちらでも可。
// アップロードするファイルは customize/test/resources に配置。
new FileType(model, "avatarImage").uploadFromClasspath("b.pdf");
// アップロードしたファイルクリア(クリアボタンをクリック)
//new FileType(model, "avatarImage").clear();
// ファイル名の確認。
new FileType(model, "avatarImage").shouldHave("a.png");
// ファイルのダウンロード。
File downloadFile = new FileType(model, "avatarImage").download();
// アップ前後のファイルが同じかを確認。
assertThat(FileUtils.contentEquals(new File("a.png"), downloadFile), is(true));
66/97
67. 複数ファイルアップロード
• 繰り返しコンテナ内のファイル型項目は複数ファイルのアップロード
に対応している(Dojo FileUploaderを利用)
• Selenide と Dojo FileUploaderの相性問題でWTFでは複数ファイルのアップ
ロードは対応できていない
• 繰り返しコンテナでも単一ファイルアップロードは可能
• 非推奨方式だが複数ファイルをアップロードする方法はある
• Dojo FileUploaderを使った方式とは異なる点
• Dojo FileUploaderを使った方式はREST APIを通してファイルをアップロード
• 非推奨方式はREST APIを用いず、マルチパートフォームでファイルをアップロード
• Wagbyのサーバー側の処理が上記2方式は大きく異なる(ファイルを受け取るメソッドが別)
• アップロードのテストにはならないがダウンロードのテストができるようになる
67/97
68. 複数ファイルアップロード
• <input type=“file” multiple … > を強制的に作成し、そちらに対して複
数ファイルのアップロードを行う。
String name = new WebModelitem<>(model, "${項目名}").elementId();
// 複数ファイルを送信するための input/@type="file" を用意する。
String id = "___jshparam___id___" + name + "___" + System.nanoTime();
SelenideElement form = item.element().closest("form");
executeJavaScript(""
+ " var fileInput = document.createElement('input');"
+ " fileInput.setAttribute('type', 'file');"
+ " fileInput.setAttribute('multiple', true);"
+ " fileInput.setAttribute('name', arguments[2]);"
+ " fileInput.setAttribute('id', arguments[1]);"
+ " fileInput.style.width = '1px';"
+ " fileInput.style.height = '1px';"
+ " arguments[0].appendChild(fileInput);",
form, id, name);
$(WebElementUtils.idSelector(id)).uploadFromClasspath("a.txt", "b.txt");
68/97
70. 入力不可項目
• shouldHave(disabled) を利用
• 入力不可(グレーアウト)となっていない場合はエラーとなる
// 入力不可項目の確認
new WebModelitem<>(model, "etc").shouldHave(disabled);
// こちらでも可。
//new WebModelitem<>(model, "etc").element().shouldBe(disabled);
// 趣味の「その他」チェックボックスを選択する
new CheckBox(model, "hobbies").val("その他");
// 入力可能状態となったことを確認
new WebModelitem<>(model, "etc").shouldHave(enabled);
70/97
72. 繰り返しコンテナ
// 繰り返しコンテナを操作するオブジェクトを作成。
WebContainerModelitem report = new WebContainerModelitem(model, "report");
// コンテナの1行目があればこれを取得する(なければエラー)。
WebContainerModelitem report01 = report.get(1);
// コンテナ1行目の「rnote」項目への入力
new WebModelitem<>(report01, "rnote").val("Wagby 購入");
// コンテナの2行目がない状態で実行するとエラーとなる。
//WebContainerModelitem report02 = report.get(2);
// コンテナの「追加」ボタンをクリックして、新規行を取得。
WebContainerModelitem report02 = report.add();
// コンテナ2行目の「rnote」項目への入力
new WebModelitem<>(report02, "rnote").val("Wagby 保守契約更新");
繰り返しコンテナ
72/97
73. データの入力、保存、確認の流れ
// 各項目へデータの入力
new WebModelitem<>(model, "name").val("ジャスミン太郎");
new ListBox(model, "customertype").val("地方公共団体");
new SearchList(model, "account").val("admin");
new WebMultiModelitem<>(model, "email")
.val("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
// 保存
save();
// ページタイトルを確認
pageTitle().shouldHave(exactText("顧客 詳細表示"));
// 保存後の値を確認
new WebModelitem<>(model, "name").shoulHave("ジャスミン太郎");
new ListBox(model, "customertype").shoulHave("地方公共団体");
new SearchList(model, "account").shoulHave("admin");
new WebMultiModelitem<>(model, "email")
.shoulHave("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
73/97
74. モデル情報の分離
• PageObjectデザインパターン
• 画面を1つのクラスとして定義
• 画面内のボタンや入力項目をPageObjectに保持
• テストシナリオクラスを別に用意し、同クラスからPageObjectを操作する方式
• WTFをこの方式に当てはめると
• 登録/更新/詳細表示画面は、ほぼ同じPageObjectとなる
• この考えを更に推し進めモデルクラスを作成
• 登録/更新/詳細画面で同じモデルクラスを利用する
• ボタンなどの操作はOperationsクラスが補助
74/97
75. モデルクラス
• 項目名と型情報はモデルクラスが保持
public class Customer extends WebModel {
/**
* コンストラクタ。
*/
public Customer() {
super("customer");
}
// 通常項目
public final WebModelitem<?> name = new WebModelitem<>(this, "name");
// リストボックス
public final ListBox customertype = new ListBox(this, "customertype");
// 他モデルの参照(検索画面)
public final SearchList account = new SearchList(this, "account");
// 繰返し項目
public final WebMultiModelitem<?> email = new WebMultiModelitem<>(this, "email");
// 時間型リストボックス
public final TimeListBox start = new TimeListBox(this, "start");
// 郵便番号
public final Postcode postcode = new Postcode(this, "postcode");
75/97
76. シナリオクラス(テストクラス)
• 入力値や画面の操作情報を実装
Customer model = new Customer(); // モデルオブジェクトの作成
// 各項目へデータの入力
model.name.val("ジャスミン太郎");
model.customertype.val("地方公共団体");
model.account.val("admin");
model.email.val("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
// 保存
save();
// ページタイトルを確認
pageTitle().shouldHave(exactText("顧客 詳細表示"));
// 保存後の値を確認
model.name.shoulHave("ジャスミン太郎");
model.customertype.shoulHave("地方公共団体");
model.account.shoulHave("admin");
model.email.shoulHave("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
76/97
77. モデルクラス(繰り返しコンテナ)
• WebContainerModelitemクラスを継承したコンテナクラスを作成
• モデルクラスの内部クラスとして作成
• コンストラクタは2種類用意し、newInstance()メソッドも必ず作成する
• コンテナ内項目は同クラス内に定義していく
public class Customer extends WebModel {
...
public final Report report = new Report(this);
public class Report extends WebContainerModelitem<Report> {
public Report(WebModel model) {
super(model, "report");
}
public Report(WebModel model, int index) {
super(model, "report", index);
}
protected Report newInstance(int idx) {
return new Report(model, idx);
}
public final WebModelitem<?> rid = new WebModelitem<>(this, "rid");
public final DateTextBox rdate = new DateTextBox(this, "rdate");
...
}
} 77/97
78. シナリオクラスでのコンテナの操作
// コンテナの1行目があればこれを取得する(なければエラー)。
Report report01 = model.report.get(1);
// コンテナ1行目の「rnote」項目への入力
report01.rnote.val("Wagby 購入");
// コンテナの2行目がない状態で実行してもエラーとなる。
//Report report02 = report.get(2);
// コンテナの「追加」ボタンをクリックして、新規行を取得。
Report report02 = model.report.add();
// コンテナ2行目の「rnote」項目への入力
report02.rnote.val("Wagby 保守契約更新");
save(); // 保存
pageTitle().shouldHave(exactText("顧客 詳細表示"));
// 繰り返しコンテナ1行目のデータを取得
report01 = model.report.get(1);
// コンテナ1行目の「rnote」項目の確認
report01.rnote.shouldHave("Wagby 購入");
// 繰り返しコンテナ2行目のデータを取得
report02 = model.report.get(2);
// コンテナ2行目の「rnote」項目の確認
report02.rnote.shouldHave("Wagby 保守契約更新"); 78/97
80. 検索
• WebConditionModelクラスを継承したモデルクラスを作成
• 範囲検索項目は下限値用項目と上限値用項目の2項目用意する
• 下限値項目は項目名末尾に “1jshparam” を付与。上限値項目は”2jshparam”を付与。
• 定義によっては登録画面と検索画面では入力のタイプが異なる項目も存在
• 例) 登録・更新画面ではリストボックス、検索画面ではチェックボックス
• 繰り返し項目や繰り返しコンテナ内項目は検索画面では通常項目と同じ扱い
• WebMultiModelitem, WebContainerModelitemを利用することはない
public class CustomerCp extends WebConditionModel {
public CustomerCp() {
super("customer");
}
// customer/customerid 項目の範囲検索(下限値)
public final WebModelitem<?> customerid1jshparam = new WebModelitem<>(this, "customerid1jshparam");
// customer/customerid 項目の範囲検索(上限値)
public final WebModelitem<?> customerid2jshparam = new WebModelitem<>(this, "customerid2jshparam");
// customer/customername
public final WebModelitem<?> name = new WebModelitem<>(this, "name");
// customer/email 検索画面では繰返し項目ではなく通常項目扱い
public final WebModelitem<?> email = new WebModelitem<>(this, "email");
80/97
81. 検索
• 検索画面テスト用シナリオクラスの実装
pageTitle().shouldHave(exactText("顧客情報 検索"));
// WebConditionModel を継承した検索条件部を表すモデル。
CustomerCp condition = new CustomerCp();
// 以下、検索条件の入力。
// 範囲検索の下限値と上限値の入力
condition.customerid1jshparam.val("1");
condition.customerid2jshparam.val("10000");
// 検索画面では繰り返し項目(WebMultiModelitem)ではなく
// 通常項目(WebModelitem)として扱われるので入力値は一つ
condition.email.val("sales@jasminesoft.co.jp");
// 「検索の実行」ボタンをクリック。
search();
pageTitle().shouldHave(exactText("顧客情報 検索"));
// 検索結果件数の確認。
$(".display_resultcount").shouldHave(text("検索条件に合致したデータは1件見つかりました。"));81/97
82. 一覧
• WebListModelクラスを継承したモデルクラスを作成
• コンストラクタは2種類用意し、newInstance()メソッドも必ず作成する
• 項目は登録・詳細画面用モデルと同じ様に作成する
• 繰返し項目もWebMultiModelitemを利用する
• 繰り返しコンテナ内項目のテストには未対応
public class CustomerLp extends WebListModel<CustomerLp> {
public CustomerLp() {
super("customer");
}
public CustomerLp(int index) {
super("customer", index);
}
@Override
protected CustomerLp newInstance(int idx) {
return new CustomerLp(idx);
}
// customer/customerid
public final WebModelitem<?> customerid = new WebModelitem<>(this, "customerid");
// customer/name
public final WebModelitem<?> name = new WebModelitem<>(this, "name"); 82/97
83. 一覧
• 一覧表示部テスト用シナリオクラスの実装
pageTitle().shouldHave(exactText("顧客情報 検索"));
// 「一覧画面へ」ボタンをクリック(検索画面と一覧画面が分かれている場合)。
clickListPageButton();
pageTitle().shouldHave(exactText("顧客情報 一覧"));
// 検索結果件数の確認。
$(".display_resultcount").shouldHave(text("1件中、1件目から1件目を表示しています。"));
// WebListModel を継承した一覧表示部を表すモデル。
CustomerLp list = new CustomerLp();
// 1行目のデータを取得
CustomerLp record01 = list.get(1);
// 通常項目
record01.name.shouldHave("ジャスミン太郎");
// 繰返し項目
record01.email.shouldHave("taro@jasminesoft.co.jp", "sales@jasminesoft.co.jp");
// 2件目のデータがあれば、以下のように取得する。
//CustomerLp record02 = list.get(2);
// 「詳細」ボタンをクリック。
record01.clickDetailButton();
pageTitle().shouldHave(exactText("顧客情報 詳細表示")); 83/97
84. 一覧更新
• WebUpdateListModelクラスを継承したモデルクラスを作成
• コンストラクタは2種類用意し、newInstance()メソッドも必ず作成する
• 項目は登録・詳細画面用モデルと同じ様に作成する
public class CustomerUlp extends WebUpdateListModel<CustomerUlp> {
public CustomerUlp() {
super("customer");
}
public CustomerUlp(int index) {
super("customer", index);
}
@Override
protected CustomerUlp newInstance(int idx) {
return new CustomerUlp(idx);
}
// customer/customerid
public final WebModelitem<?> customerid = new WebModelitem<>(this, "customerid");
// customer/name
public final WebModelitem<?> name = new WebModelitem<>(this, "name");
84/97
85. 一覧更新画面でのデータの入力
• 一覧更新画面データ入力テスト用シナリオクラスの実装
• データ登録後の確認は一覧画面で行う
pageTitle().shouldHave(exactText("顧客情報 検索"));
// 「一覧更新へ」ボタンをクリック。
clickUpdateListPageButton();
pageTitle().shouldHave(exactText("顧客情報 一覧更新"));
// WebUpdateListModel を継承した一覧更新部を表すモデル。
CustomerUlp updateList = new CustomerUlp();
// 1行目のデータを取得
CustomerUlp record01 = updateList.get(1);
// 1行目の「コピー」ボタンをクリック。コピーされた2行目のデータが取得できる。
CustomerUlp copyRecord02 = record01.clickCopyButton();
// 名前を変更する。
copyRecord02.name.val("ジャスミン次郎(※一覧更新画面で作成)");
...
// 2行目の「新規」ボタンをクリック。3行目の新規データが取得できる。
CustomerUlp newRecord03 = copyRecord02.clickNewButton();
newRecord03.name.val("ジャスミン三郎(※一覧更新画面で作成)");
...
//「保存」ボタンをクリック
save();
// メッセージチェック。
infos().shouldBe(size(1));
infos().shouldHave(exactTexts("1件の顧客情報データを新規登録しました。1件の顧客情報データをコピー登録しました。"));
pageTitle().shouldHave(exactText("顧客情報 一覧")); 85/97
88. ワークフローのテスト
• テスト流れ(例:年休申請)
1. 申請用ユーザーでログオン
2. 年休データの登録(申請フローの指定)
3. 申請処理を行う(ワークフロー開始)
4. 申請用ユーザーはログオフし、承認用ユーザーでログオン
5. 対象となる年休データの表示
6. 承認処理を行う
7. 承認用ユーザーはログオフし、 決裁用ユーザーでログオン
8. 対象となる年休データの表示
9. 決裁処理を行う
10. 決裁用ユーザーのログオフ
• 赤字の部分がこれまでと異なる処理
• 残りの処理はこれまでに紹介した方法でテスト可能
88/97
90. 申請状況の確認
• 申請状況
• 年休モデル詳細画面の下部に表示される
// 申請状況一覧の1行目のデータを確認。
JfcworkstateLp record01 = workflow().getWorkstate(1);
record01.username.shouldHave("申請ユーザー"); // 処理者
record01.event.shouldHave(“新規登録”); // 処理内容
record01.comment.shouldHave(empty); // コメント
90/97
92. テストクラスの構成
• クラス、メソッドの分割の指針(参考情報)
• テストは再実行することを意識して作成する
• テストに失敗した場合、途中から再実行することになる
• テストはメソッド単位で再実行可能
• データの登録などは1メソッド1件にすると再実行しやすい
• @FixMethodOrderアノテーションで複数のテストメソッドの実行順序を制御
• JUnitのデフォルトの動作では複数テストメソッドの実行順序は保証されない
• 1クラス1テストテーマ
• 1テーマ内のテスト項目が多い場合は複数クラスに分割する
• クラスの最初のメソッドでログオン、最後のメソッドでログオフ
• 前のテストクラスでログオンしていることを前提にしない方がよい
• クラスの先頭メソッドにログオン処理があると、どのユーザーでテスト実行しているかを把握し
やすい
• @AfterClassを付与したメソッドでは念のため logoff.do でログオフする
• エラーの発生の仕方によっては最後のlogoff()が動作しないことがある
• モデルクラスとシナリオクラスはパッケージ名を別にする
• シナリオクラス: jp.jasminesoft.wagby.tests.t01_Customer.CustomerTest
• モデルクラス: jp.jasminesoft.wagby.tests.models.customer.Cusomer
92/97
93. JUnit4テスト・スイートの利用
• テスト・スイートクラスを用意
• テスト・スイートクラスはテストの実装は行わず、まとめて実行するテストクラ
スを羅列するのみとする
• SelenideのConfigurationなどはテスト・スイートクラスでやっておく。
@RunWith(Suite.class)
@SuiteClasses({
jp.jasminesoft.wagby.tests.ApplicationTest, // 申込テスト
jp.jasminesoft.wagby.tests.OrderInputTest, // 発注情報入力テスト
jp.jasminesoft.wagby.tests.OrderConfirmTest, // 発注内容確認テスト
jp.jasminesoft.wagby.tests.OrderTest, // 発注テスト
})
public class AllTests {
/** コンストラクタ */
protected AllTests() {
}
@BeforeClass
public static void setUpClass() {
Configuration.baseUrl = "http://localhost:8921/wagby";
Configuration.browser = WebDriverRunner.CHROME;
...
}
@AfterClass
public static void tearDownClass() {
open("/logoff.do"); // 念のためログオフ
}
}
93/97
95. ヘッドレスモードでの実行
• Google Chrome59から搭載されたUIなしで動作するモード
• 自動テストやWebスクレイピングでの利用を目的
• ブラウザの起動が高速化
• テスト実行時間の短縮
• オマケ: Configuration.fastSetValue = true
• 文字の一括入力によるパフォーマンス向上(デフォルトは1文字ずつ入力)
@BeforeClass
public static void setUpBeforeClass() throws Exception {
// テストの前処理を記述
Configuration.baseUrl = "http://localhost:8921/wagby"; // テスト対象システムのURL
Configuration.browser = WebDriverRunner.CHROME; // 利用するブラウザ
Configuration.headless = true; // ヘッドレスモードでの実行
// WebDriver ファイルの指定
System.setProperty("webdriver.chrome.driver","customize/chromedriver.exe");
}
95/97
96. Selenideプログラミング
• Wagbyのすべての操作をWTFで行う(理想)
• Wagbyの機能はどんどん増えているのでWTFが追いついていないのが現状
• 細かい部分はSelenideレベルのプログラミングでカバーする
// ユーザー名"admin"のアカウント詳細画面を開く(URL直接指定)
Selenide.open("/showJuser.do?userid=admin");
// 画面キャプチャを取得(ファイルはbuild/reports/tests/ss01.pngとして保存)
Selenide.screenshot("ss01");
// CSSセレクタを使い、単一要素を取得(複数存在する場合は先頭の要素)
Selenide.$(".display_resultcount").shouldHave(
text("検索条件に合致したデータは1件見つかりました。"));
// CSSセレクタを使い、複数要素を取得
Selenide.$$(".display_table th")...;
// Selenium の WebDriver を取得し、Selenium プログラミングも可能
WebDriverRunner.getWebDriver()...;
96/97