Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Wagby Testing Framework

149 views

Published on

Wagby R8.4.2

Published in: Software
  • Be the first to comment

  • Be the first to like this

Wagby Testing Framework

  1. 1. Wagby Testing Framework R8.4.2 対応版 1/97
  2. 2. Wagby Testing Frameworkとは • Wagby Testing Framework(WTF)とは、Wagby で E2E(End to End)テス トを作成するためのフレームワークです。 2/97
  3. 3. 回帰テスト 3/97
  4. 4. ローコード開発 • 企業のスピード経営を実現するための手法 • 現在の企業経営はシステムと密接に関連 • 経営方針の変更に合わせてシステムの変更も必須 • システムの改修が遅れるとビジネスチャンスを逃すことに • 経営の変化に迅速に対応する手法としてローコード開発が誕生 4/97
  5. 5. ローコード開発とテスト • システムの変更にはテストが必須 • 「システム変更は早い」が「不具合も多い」ではNG • ローコード開発では大規模な修正を短時間で実現できる • しかし、テストの範囲が広がり、テスト工数が増える • ローコード開発には効率のよいテストが求められる • 「テストをしっかりやります。ただし、納期も伸びます」はNG • テスト時間がボトルネックになり、システム変更に消極的になるのは望ましく ない • 逆にテストがしっかりしていると積極的な変更も可能 5/97
  6. 6. Wagbyアプリケーションへのテストの考え方 • Wagbyの標準機能は基本的にテスト不要 • 複雑な自動計算式や式を利用した画面制御、複雑な機能の組み合わせは 念のためテストが必要 • 式が誤っている可能性 • 解釈の違いにより動作が想定と異なる • この機能とこの機能を組み合わせればこのような動作となる(はずだ) • この機能を組みわせても今までの動作は変わらない(はずだ) • 最初の定義の時のみテストすればよいのか • ではなく、他の機能を追加しても動作が変わらないことを確認するテストが必要 6/97
  7. 7. 回帰テスト 出典 : www.weblio.jp 7/97
  8. 8. 回帰テストの必要性 • 予想していない部分への悪影響を確認するためのもの • 変更部分は誰もが注意してテストする • ただし、影響がない(だろう) 部分のテストが疎かになる • Wagbyはシステムの動作変更が容易 • しかし、その影響範囲はわかりにくくなっている • 他の開発手法より回帰テストは重要 8/97
  9. 9. 回帰テストの分類 • ユニット(単体)テスト • 小さな機能に対して細かなパターンを多くテストする • 自動化が容易 • E2Eテスト(結合テスト/総合テスト) ※ (総合テスト = 統合テスト = システムテスト) • End to End テスト • 画面遷移や複数機能の組合せ等の大きな流れをテスト • テストは手動で行われることも多い(ただし、人為的ミスも) 9/97
  10. 10. 回帰テストの実施方法 • ユニットテスト • プログラムでテストを記述 • 短時間で多くのテストを実施可能 • Wagby では基本不要 • カスタマイズコードや複雑な自動計算などを実装した場合にのみ行う • E2Eテスト • ブラウザを使って画面操作を行いながらの確認 • 一つのテストに時間がかかることが多い 10/97
  11. 11. テストの自動化 11/97
  12. 12. テストの自動化 • 自動化の範囲 1. とりあえず全部(理想) 2. 優先度の高いものから 3. 必要最低限 4. 自動化せず手動で行う 12/97
  13. 13. テストプログラムのメンテナンス • メンテナンスコスト • テストコードも(当然)メンテナンスコストがかかる • 保守しやすいテストコードとなるように心がける • 回帰テストの適用範囲を「すべて」にしてしまうと(当然)メンテナンスコストも 上がる • 手動テストと自動テストのバランスを考える 13/97
  14. 14. 安定したテスト • 自動テストには安定性も必要 • テストが不安定だと誰もテスト結果を見なくなる • テストにかかる時間も重要 • 1回のテストに何十時間もかかるのはNG • テストサーバーの分散により解決は可能 14/97
  15. 15. テストの自動化フレームワーク • ユニットテスト • JUnit • Javaテスティングフレームワークのデファクト • E2Eテスト • Selenium • ブラウザテスト自動化のデファクト 15/97
  16. 16. 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. 17. Selenium の問題点 • 複雑なことをするとコードの記述量が増える • テストの安定性に欠ける • テストコードが速く動きすぎて描画が追いつかずエラー • JavaScriptを使って描画を行う(非同期)アプリケーションでは特に顕著 • Implicit Wait(暗黙的な待機)機能では対応できないケースが多い • 解決策 : テストコードに wait を明示 17/97
  18. 18. Selenide • Selenide (http://selenide.org) • Seleniumを拡張したJavaテスティングフレームワーク • Selenium: ブラウザを操作することを目的(Low-Level API) • Selenide : ブラウザテストに特化 • 少ない記述量で実装可能 • 非同期操作のサポート(waitの記述が不要) • デフォルトで4秒waitするようになっている • 4秒間100ミリ秒間隔でリトライする(変更可能) • 描画が行われれば4秒待たずにテストが実行される • 異常時にスクリーンショットとhtmlが自動保存される 18/97
  19. 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. 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
  21. 21. Wagbyアプリケーションへの 自動テスト 21/97
  22. 22. WagbyでのE2E自動テスト • シンプルなHTMLではない • JavaScriptライブラリ「Dojo Toolkit」を採用 • Dojoがもつ豊富なUIパーツを利用 • ボタンなども <input type=“button” ..> ではなく、<span>の入れ子で表現されている • HTML構造が複雑なのでE2Eテストの難易度は高い 22/97
  23. 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. 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
  25. 25. リストボックス //開始時刻の選択。「6:30」を指定する。 //時間の選択。STEP1:▼をクリックしてプルダウンを表示 $("#customer_p¥¥$002fstart_hour") .find(byValue("▼ ")).click(); //時間の選択。STEP2:プルダウンから時間を選ぶ $$("#customer_p¥¥$002fstart_hour_menu td") .filter(exactText("6")).first().click(); //分の選択 $("#customer_p¥¥$002fstart_minute") .find(byValue("▼ ")).click(); $$("#customer_p¥¥$002fstart_minute_menu td") .filter(exactText("30")).first().click(); 時間型リストボックス 25/97
  26. 26. パターン化されたHTML • 複雑なHTMLの中にもパターンがある • idの命名規則 • 入力フィールド: cucstomer_p¥¥$002fname • 時間リストボックスの「時」: customer_p¥¥$002fstart_hour • 時間リストボックスの「分」: customer_p¥¥$002fstart_minute • リストボックスの操作パターン • STEP1:▼をクリックしてプルダウンを表示 • STEP2:プルダウンから時間を選ぶ 26/97
  27. 27. Wagby Testing Framework(WTF) • Wagbyに特化したTesting Frameworkを提供 • パターン化された共通部分は記述不要に • 編集画面での入力もシンプルに • 長いIDの記述は不要 • 通常項目は、項目名と値のみを指定 • 各種ボタン操作のためのメソッドを用意 • ボタンに付与されたidを気にする必要はない • 提携処理を一つのメソッドで • ログオン画面にアクセス > ユーザー名、パスワードの入力 > ログオンボタンをクリック • 「キャンセル」ボタンをクリック > 確認ダイアログで「OK」をクリック 27/97
  28. 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
  29. 29. テストクラスの作成 • JUnitテストクラスを作成する • WagbyにはJUnit4が同梱(サンプルコードはJUnit4ベース) • テストクラスの配置場所 customize/test/java • いくつかのクラスを static import しておく • テストの前処理として以下を記述(Selenide用設定) • テスト対象システムのURLを指定 • テストに用いるブラウザの指定(WTFはchrome限定) • WebDriverファイルの指定 29/97
  30. 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. 31. シンプルなテスト • ログオンしてメニューが表示されることを確認するだけのテスト public class MyFirstTest { ... @Test public void test() { // ログオン logon("admin", "wagby"); // メニュー画面が表示されていることを確認 pageTitle().shouldHave(exactText("メニュー")); // ログオフ logoff(); // ログオン画面が表示されていることを確認 assertThat(title(), is("Wagby アプリケーション ログオン")); } } 31/97
  32. 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. 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
  34. 34. Wagby Testing Framework フレームワークが提供するAPIの詳細 34/97
  35. 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
  36. 36. ログオン(selenide) • ログオン後の確認 // ページタイトルを確認 $("div.pagetitle").shouldHave(exactText("メニュー")); // エラーが無いことを確認 $("div.errormsg").shouldNot(exist); メニュー画面の簡易HTML メニュー画面 <html> <body> <div class="pagetitle">メニュー</div> … </body> </html> 36/97
  37. 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. 38. 操作と確認 • テストは操作と確認の繰り返し • ログオン操作 : logon("admin", "wagby") • 操作後のページタイトルの確認 : pageTitle().shouldHave(exactText("メニュー")) • データの入力 → 確認 • 画面遷移ボタンのクリック → 遷移後の画面名の確認 • 画面遷移操作後は必ずページタイトルを確認する 38/97
  39. 39. エラーチェック(selenide) • エラーメッセージの存在チェック // エラーメッセージが表示されていないことを確認 $("div.errormsg").shouldNot(exist); <div class="errormsg“> 新規パスワード は必須項目となっています。 </div> エラーメッセージが表示されている画面エラーメッセージのHTML 39/97
  40. 40. エラーチェック(WTF) • Operations#checkNoErrors() メソッド • 画面上にエラーメッセージが無いこと及びJavaScriptのエラーが発生してい ないことを確認。 • checkNoErrors() の自動呼び出し(暗黙的な確認) • Operations クラスが提供する操作用メソッドでは、操作後のエラーメッセー ジ確認のため、checkNoErrors()が自動的に呼び出される。 // エラーが無いことを確認します。 checkNoErrors(); // ログオン処理を行う(内部でcheckNoErros()を自動実行)。 logon("admin", "admin"); // checkNoErrors(); // 個別実行は不要。 40/97
  41. 41. 異常系のテスト(WTF) • エラーメッセージの内容確認 • 例)初期パスワードでのログオン • logon()メソッドの第三引数にfalseを指定す ることでcheckNoErrors()を呼び出さない • エラー内容をチェックするコードをテストと して実装する import static com.codeborne.selenide.CollectionCondition.*; … // ログオン処理を行う(checkNoErros()は行わない)。 logon("user01", "user01", false); // エラーメッセージの完全一致チェック。 errors().shouldHave(exactTexts("初期パスワードがセットされていま す。パスワードの変更を行ってください。パスワードを変更するまでは、操作が 制限される可能性があります。")); 41/97
  42. 42. 異常系のテスト(WTF) • 複数のエラーメッセージのチェック import static com.codeborne.selenide.CollectionCondition.*; … // 複数エラーメッセージの完全一致チェック。 errors().shouldHave(exactTexts( "エラーメッセージ01", "エラーメッセージ02", "エラーメッセージ03")); // 複数エラーメッセージの部分一致チェック。 errors().shouldHave(texts( "メッセージ01", "メッセージ02", "メッセージ03")); 42/97
  43. 43. 異常系のテスト(WTF) • エラーコードでのチェック • エラーメッセージが変更されるとテストに失敗する • エラーメッセージのHTMLにはエラーコードが含まれている(Wagbyの独自属 性) // エラーコードでのチェック。 errors().shouldHave(msgcodes("error.init.passwd")); <div class="errormsg" msgcode="error.init.passwd"> 初期パスワードがセットされて... </div> エラーメッセージのHTML 43/97
  44. 44. ログオフ(WTF) ログオフボタン import static com.codeborne.selenide.Selenide.*; // ログオフ処理を行う。 logoff(); // checkNoErrors(); // 不要。 // HTMLのタイトルを確認 assertThat(title(), is("Wagby アプリケーション ログオン")); 44/97
  45. 45. メニュー(WTF) メインメニュー サブメニュー // メインメニューでの画面遷移 // 「サービス」タブを選択後、「顧客検索」メニューをクリック selectMenu("サービス", "顧客検索"); // ページタイトルを確認 pageTitle().shouldHave(exactText("顧客 検索")); // サブメニューでの画面遷移 // 大項目「サービス」を選択後、プルダウンから「顧客検索」を選択する selectSubMenu("サービス", "顧客検索"); pageTitle().shouldHave(exactText("顧客 検索")); 45/97
  46. 46. 登録画面への遷移 • 登録画面への遷移ボタン • ボタンのid値は”btnNewInsertCustomer” // 「登録画面へ」ボタンをクリック clickNewButton("customer"); // 登録画面用ボタンが一つの場合は"customer"は省略可 clickNewButton(); //clickEditButton(); //更新用(ルールは「登録画面へ」のボタンと同じ) 登録画面への遷移ボタンが一つのみ存在 登録画面への遷移ボタンが複数存在 46/97
  47. 47. 保存/キャンセル処理 • 保存処理 • キャンセル処理 // 「保存」ボタンをクリック save(); // ページタイトルを確認 pageTitle().shouldHave(exactText("顧客 詳細表示")); 保存/キャンセルボタン //「キャンセル」ボタンをクリック。クリック後に表示される確認ダイアログも //自動的にOKをクリックします。 cancel(); // ページタイトルを確認 pageTitle().shouldHave(exactText("顧客 詳細表示")); 47/97
  48. 48. 保存後のメッセージの確認 • 正常終了メッセージの確認(暗黙的な確認) // 「保存」ボタンをクリック save(); // 正常終了メッセージが表示されていることを確認。 // ただし、このメソッドはsave()メソッド呼び出し時に自動実行されるため、 // 通常は明示的に呼び出す必要はない。 checkSuccessMessages(); 48/97
  49. 49. Operationsクラスが提供するメソッド • 登録・更新・コピー登録画面用 • 詳細・検索・一覧画面用 メソッド名 処理内容 clickNewButton() 「登録画面へ」ボタンをクリックする。 clickEditButton() 「更新画面へ」ボタンをクリックする。 clickDeleteButton() 「削除」ボタンをクリックする。 clickCopyButton() 「コピー登録へ」ボタンをクリックする。 clickDetailButton() 外部キー親モデルの詳細表示画面へ遷移するためのボタンを クリックする。 clickSearchPageButton() 「検索画面へ」ボタンをクリックする。 clickListPageButton() 「一覧画面へ」ボタンをクリックする。 search() 検索画面の「検索の実行」ボタンをクリックする。 resetSearch() 検索画面の「リセット」ボタンをクリックします。 clickUpdateListPageButton() 「一覧更新へ」ボタンをクリックする。 メソッド名 処理内容 save() 「保存」ボタンをクリックする。 cancel() 「キャンセル」ボタンをクリックする。 49/97
  50. 50. Operationsクラスが提供するメソッド • メニュー • グローバルリンク メソッド名 処理内容 selectMenu(“タブ名”, “メニュー名”) 指定のメニューを選択する。 selectSumMenu(“グループ名”, “メニュー名”) 指定のメニューを選択する。 メソッド名 処理内容 menu() 「メニューへ戻る」をクリックする。 logoff() 「ログオフ」をクリックする。 changeFontSize(size) * sizeはlarge,medium,smallを指定 「XXXフォントサイズ」をクリックする。 50/97
  51. 51. Operationsクラスが提供するメソッド • ウィザード画面用 メソッド名 処理内容 clickWizardNextButton() 「次画面へ画面へ」ボタンをクリックする。 clickWizardPrevButton() 「前画面へ画面へ」ボタンをクリックする。 51/97
  52. 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. 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. 54. リストボックス • ListBoxクラスを利用 • 通常項目と同様に val() メソッドでも 値のセットは可能。 // リストボックス:「地方公共団体」を選択する new ListBox(model, "customertype") .dropdown() .select("地方公共団体"); // こちらでも可(内部処理は同じ) new ListBox(model, "customertype") .val("地方公共団体"); リストボックス 54/97
  55. 55. ラジオボタン/チェックボックス • それぞれの型に応じたクラスを利用 // ラジオボタン:「地方公共団体」を選択する new RadioButton(model, "customertype").val("地方公共団体"); // チェックボックス:「民間企業」と「地方公共団体」を選択する // チェックボックスは複数の値の指定が可能 new CheckBox(model, "customertype") .val("民間企業", "地方公共団体"); ラジオボタン 55/97
  56. 56. 他モデルの参照(検索画面) • サブウィンドウを開く SearchList account = new SearchList(model, "account"); // サブウィンドウを操作するオブジェクトを取得。 SubWindow subWindow = account.subWindow(); // サブウィンドウを表示する。 subWindow.show(); 56/97
  57. 57. 他モデルの参照(検索画面) • サブウィンドウの一覧表示部から値を選ぶ • サブウインドウの表示から値の選択までを一括で処理 // サブウィンドウの一覧画面に表示されている "admin" のリンクをクリック。 subWindow.clickLink("admin"); // フォーカスをメインウィンドウに戻す。 WebElementUtils.focusMainWindow(); // こちらでも可(内部処理は同じ) new SearchList(model, "account").val("admin"); 57/97
  58. 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. 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. 60. さまざまな種類の項目への対応 • 全て val() で入力、shouldHave()で確認可能 項目の種類 対応するJavaクラス 入力フィールド WebModelitem リストボックス ListBox ラジオボタン RadioButton チェックボックス CheckBox 検索画面(サブウィンドウ) SearchList 日付入力フィールド(DatePicker) DateTextBox 日付リストボックス DateListBox 時間リストボックス TimeListBox 追記型リストボックス ComboBox 郵便番号 Postcode 繰返し項目 WebMultiModelitem 60/97
  61. 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
  62. 62. val()以外の操作が必要な入力項目 • 郵便番号と住所の同期 • DatePickerでの日付入力 • 追記型リストボックスの選択 • 読み込み専用項目の状態確認 • 入力不可項目の状態確認 • 閲覧権限による入力項目の存在チェック • 繰り返しコンテナの追加・削除の操作 62/97
  63. 63. 郵便番号と住所の同期 • Postcodeクラスを利用 • sync()メソッドで住所の同期を行う // 郵便番号と住所の同期 new Postcode(model, "postcode").val("901-2227").sync(); // 住所項目の値を確認 new WebModelitem<>(model, "address") .shouldHave("沖縄県宜野湾市宇地泊"); 郵便番号と住所 63/97
  64. 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. 65. 追記型リストボックス • ComboBoxクラスを利用 // 追記型リストボックス ComboBox companyname = new ComboBox(model, "companyname"); // 直接入力を行う場合(存在しない選択肢も可) companyname.val("ジャスミンソフト"); // ドロップダウンから選択する場合 // 存在しない選択肢を選んだ場合はエラー companyname .dropdown() .select("ジャスミンソフト"); 追記型リストボックス 65/97
  66. 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. 67. 複数ファイルアップロード • 繰り返しコンテナ内のファイル型項目は複数ファイルのアップロード に対応している(Dojo FileUploaderを利用) • Selenide と Dojo FileUploaderの相性問題でWTFでは複数ファイルのアップ ロードは対応できていない • 繰り返しコンテナでも単一ファイルアップロードは可能 • 非推奨方式だが複数ファイルをアップロードする方法はある • Dojo FileUploaderを使った方式とは異なる点 • Dojo FileUploaderを使った方式はREST APIを通してファイルをアップロード • 非推奨方式はREST APIを用いず、マルチパートフォームでファイルをアップロード • Wagbyのサーバー側の処理が上記2方式は大きく異なる(ファイルを受け取るメソッドが別) • アップロードのテストにはならないがダウンロードのテストができるようになる 67/97
  68. 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
  69. 69. 読み込み専用項目 • shouldBeReadOnly()メソッドを利用 • 読込専用となっていない場合はエラーとなる // 読込専用項目の確認:最終更新者 new WebModelitem<>(model, "updatedBy") .shouldBeReadOnly() .shouldHave("admin"); // 読込専用項目の確認:データ作成日 new WebModelitem<>(model, "createdAt") .shouldBeReadOnly() .shouldHave("2017-01-01 00:00:00"); 69/97
  70. 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
  71. 71. 入力フィールドの存在確認 • 権限により項目が非表示となっていることを確認 • 登録・更新・コピー登録画面 • 詳細表示画面 // 権限により非表示となっている new WebModelitem<>(model, "secret_memo") .element().shouldNotBe(exist); // 権限により非表示となっている new WebModelitem<>(model, "secret_memo") .contentElement().shouldNotBe(exist); 71/97
  72. 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. 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. 74. モデル情報の分離 • PageObjectデザインパターン • 画面を1つのクラスとして定義 • 画面内のボタンや入力項目をPageObjectに保持 • テストシナリオクラスを別に用意し、同クラスからPageObjectを操作する方式 • WTFをこの方式に当てはめると • 登録/更新/詳細表示画面は、ほぼ同じPageObjectとなる • この考えを更に推し進めモデルクラスを作成 • 登録/更新/詳細画面で同じモデルクラスを利用する • ボタンなどの操作はOperationsクラスが補助 74/97
  75. 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. 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. 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. 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
  79. 79. システムモデル(組み込みモデル) • 以下のシステムモデルはフレームワークに組み込まれている • フロー参加者設定 : jfcparticipant_setting • ワークフロー設定 : jfcworkflow_setting • フローイベント : jfcworkstate • 開始フロー状態 : jfcstartworkstate (new R8.4.2) • 保留フロー状態 : jfcsuspendworkstate (new R8.4.2) • その他も随時追加予定 79/97
  80. 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. 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. 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. 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. 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. 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
  86. 86. 一覧更新画面でのデータの削除 • 一覧更新画面データ削除テスト用シナリオクラスの実装 • 削除用チェックボックスを有効にして保存処理を行う pageTitle().shouldHave(exactText("顧客情報 検索")); // 「一覧更新へ」ボタンをクリック。 clickUpdateListPageButton(); pageTitle().shouldHave(exactText("顧客情報 一覧更新")); // 以下、表示されているデータ全てを削除する。 CustomerUlp updateList = new CustomerUlp(); // データ件数を取得。 int rowSize = updateList.size(); // データの件数分ループ処理を行う。 for (int i = 1; i <= rowSize; i++) { // i行目の「削除」チェックボックスをクリック。 updateList.get(i).clickDeleteCheckBox(); } //「保存」ボタンをクリック save(); pageTitle().shouldHave(exactText("顧客情報 一覧")); 86/97
  87. 87. ワークフローのテスト • テストの前提 • 年休申請ワークフロー • フローパターンは「申請→承認→決裁」 • 各処理を行うユーザー • 申請: applicationUser(申請ユーザー) • 承認: admitUser(承認ユーザー) • 決裁: approvalUser(決裁ユーザー) 87/97
  88. 88. ワークフローのテスト • テスト流れ(例:年休申請) 1. 申請用ユーザーでログオン 2. 年休データの登録(申請フローの指定) 3. 申請処理を行う(ワークフロー開始) 4. 申請用ユーザーはログオフし、承認用ユーザーでログオン 5. 対象となる年休データの表示 6. 承認処理を行う 7. 承認用ユーザーはログオフし、 決裁用ユーザーでログオン 8. 対象となる年休データの表示 9. 決裁処理を行う 10. 決裁用ユーザーのログオフ • 赤字の部分がこれまでと異なる処理 • 残りの処理はこれまでに紹介した方法でテスト可能 88/97
  89. 89. データの登録 • 申請フローの指定 • 申請フロー項目はWagbyが自動的に作成した特殊な項目 • 以下のコードで指定可能 workflow().participants_id.val("年休申請フロー"); 89/97
  90. 90. 申請状況の確認 • 申請状況 • 年休モデル詳細画面の下部に表示される // 申請状況一覧の1行目のデータを確認。 JfcworkstateLp record01 = workflow().getWorkstate(1); record01.username.shouldHave("申請ユーザー"); // 処理者 record01.event.shouldHave(“新規登録”); // 処理内容 record01.comment.shouldHave(empty); // コメント 90/97
  91. 91. 申請・承認・決裁処理 • 申請・承認・決裁処理の実行 • 年休モデル詳細画面の下部に表示されるボタンの操作 • 申請・承認・決裁処理ボタン押下後に画面上部に表示される「ワークフロ処 理は正常に行われました」メッセージの存在チェックは自動的に行われる workflow().comment.val("年休申請します。"); // コメント workflow().application(); // 申請ボタンをクリック。 91/97
  92. 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. 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
  94. 94. JUnit4テスト・スイートの利用 • テストクラスの例 • SelenideのConfigurationは行わずテストのみ実装 @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class ApplicationTest { @Test public void test01_ログオン() { logon(“ApplicationUser”, ...); // 申し込みユーザーログオン pageTitle().shouldHave(exactText("メニュー")); } @Test public void test02_申し込みデータの登録() { ... } @Test public void test03_申し込みデータの登録2() { ... } ... @Test public void test99_ログオフ() { logoff(); } } 94/97
  95. 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. 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
  97. 97. バージョンアップによる対応 • WagbyのバージョンアップによるHTMLの違いを吸収 • 例) R8.4.0 アカウント登録画面のプリンシパルチェックボックスのタブ化 • タブが導入されても以前のテストコードは変更不要 new CheckBox(model, "jprincipalId").val("共通処理", "一般ユーザ"); R8.3.X以前 R8.4.0以降 97/97

×