自動打鍵テスト - 画面操作・検証だけでなくテストコード自体も自動生成してしまう真の自動化

| 12 min read
Author: tadahiro-imada tadahiro-imadaの画像

イントロ

#

この記事では、豆蔵が支援するプロジェクトでのWebアプリに関するテスト自動化の事例を紹介します。
特にそのプロジェクト特有という内容ではなく、どのプロジェクトでも共通するポイントについて具体例を交えてまとめました。

タイトルにある「自動打鍵テスト」とは

#

WebアプリのテストをSelenium[1]やPuppeteer[2]、Playwright[3]などを利用することにより、人の手による操作(キー入力やマウスクリック)を必要とせず、人の目による確認を必要としない意味で自動化したものをさします(一般的にE2Eテストの操作を自動化したもの)。

さらに操作用コードも大部分を自動生成したり、HTMLやコミットされたDBスキーマの更新情報(画面に現れない情報)を自動検証したり、アプリの変更を検知してJenkinsで自動実行したりするなど、不具合の検出を自動化することまでを範囲とします。

「自動操作テスト」と呼んだ方が一般的かもしれないですが、システム開発現場の一部でアプリを操作することを「打鍵する」と呼んでいるのと、「自動打鍵」という響きが気に入ってこう呼んでいます。

Information

ブラウザーの自動操作についてはSeleniumを筆頭に、PuppeteerやPlaywrightなど楽しそうなものがいくつかあります。

この記事ではSeleniumを使ったケースを書いていますが、特にどのライブラリーでなければできないというような話はありませんので、対応ブラウザーや細かい機能に応じて読み替えてもらえればと思います。

操作と検証を自動化するメリット

#

Webアプリのテストに限った話ではありませんが、アプリの操作や検証を人に頼らず自動化することのメリットには、以下のようなものがあります。

  • ブラウザ操作をプログラム化することで、操作ミスをなくすことができる。
  • テストに要する工数が削減できる。
  • 長時間のテストや、夜間のテスト、複数アプリ同時にテストなどもできる。
  • 機械によるチェックを行うため、人間では見逃してしまうような検証漏れを防ぐことができる。
  • 画面からDBまで結合したテストを行うことで、画面に表示されないような変更も検出できる。

あるプロジェクトの例

#

この記事で解説するWebアプリは、以下のようなものです。

  • Webアプリ
    • WebLogic(Tomcat)で動くJSPベースのアプリ
  • DB
    • Oracle
  • ブラウザ
    • 当初IE→現在EdgeのIE互換モード、一部Chromeも対応

ページオブジェクトデザインパターンの採用

#

あるプロジェクトではページオブジェクトデザインパターン[4]を採用しました。
ページオブジェクトクラスは、対応する画面上で実行可能なすべての操作をそれぞれの操作に対応するメソッドとして提供するクラスです。

画面(=各画面のページオブジェクトクラス)ごとに操作(=メソッド)をあらかじめ定義しておくことで、操作手順にしたがって、それらを順に呼び出すだけでテストシナリオが実装できるようになります。メソッドの返り値を画面のページオブジェクトにすることで、メソッドチェーンで分かりやすいテストコードが書けるようになります。
画面遷移をする操作の場合は、遷移後の画面のページオブジェクトを返すように記述します。

テスト用のクラス、メソッドはあえて日本語名を採用

#

画面名やボタン名などは日本語ラベルなので、クラス名やメソッド名を日本語にします。画面がわかっていれば、次の例のようにテストコード(コメントは本来不要です)を見ただけで何をしているかわかるようになります。

テスト対象の画面と操作は以下のようになっています。

テスト対象
テスト操作

テストコードの例(コメントは本来不要です)を示します。

public class 顧客管理Test extends BaseTest {
  @Test
  public void test顧客を検索する() {
    login("user01") // 認証の操作は共有化しておく

      .___顧客管理メニュー画面___() // thisを返すだけの画面名を表すメソッド
      .顧客検索() // 1.「顧客検索」リンクをクリック

      .___顧客検索画面___()
      .担当社員検索() // 2.入力補助ボタンをクリックし、入力補助ダイアログを開く

      .___社員検索ダイアログ___()
      .氏名("山田") // 3.氏名の検索条件(テキストボックス)に入力
      .社員検索()
      .選択(1) // 4.入力補助ダイアログで1件選択するとダイアログが閉じる

      .___顧客検索画面___()
      .顧客検索() // 5.「顧客検索」ボタンクリック
      .次ページ() // 6.リンクでページングして動作確認
      .前ページ() // 7.リンクでページングして動作確認
      .顧客番号("C0000001") // 8.一覧のリンクをクリック

      .___法人顧客詳細画面___() // 詳細画面に遷移
      .閉じる();
  }
}

Information

ページオブジェクトデザインパターンはSeleniumのサイト[4:1]でも推奨されているもので、テスト対象のアプリケーションの画面を1つのオブジェクトとしてとらえるデザインパターンです。

画面の操作をページオブジェクトに隠蔽し、テストケースと画面操作を分離することでコードの重複を防ぎ、画面に修正があってもテストコードの変更が最小限で済むような保守性の高いテストコードが書けるようになります。

参考までに、ページオブジェクトパターンの原則としては以下のものがあります。

  • The public methods represent the services that the page offers
    publicメソッドは、ページが提供するサービスを表す
  • Try not to expose the internals of the page
    ページの内部を公開しない
  • Generally don't make assertions
    通常はアサーションを作らない
  • Methods return other PageObjects
    メソッドは他のPageObjectsを返す
  • Need not represent an entire page
    ページ全体を表現する必要はない
  • Different results for the same action are modelled as different methods
    同じ動作で異なる結果になるものは、別のメソッドを作る

ページオブジェクトコードの例を示します。

public class 顧客検索画面 extends BasePageObject<顧客検索画面> {
  public 顧客検索画面 ___顧客検索画面___() {
    return this;
  }

  public 顧客検索画面 顧客検索() {
    click(By.id("customerSearchButton"));
    // 後述の自動化の3.3(勝手に検証)
    return refresh();
  }

  public 社員検索ダイアログ 担当社員検索() {
    click(By.id("userSearchButton"));
    // 後述の自動化の2(画面切り替え)、自動化の3.3(勝手に検証)
    return openPopupPage(社員検索ダイアログ.class);
  }

  public 顧客検索画面 担当社員番号(String userNo) {
    sendKeys(By.id("userNo"), userNo);
    return this;
  }

  public 法人顧客詳細画面 顧客番号(String customerNo) {
    click(By.linkText(customerNo));
    // 後述の自動化の2(画面切り替え)、自動化の3.3(勝手に検証)
    return createPage(法人顧客詳細画面.class);
  }
}

このプロジェクトで豆蔵が作った自動打鍵テストのサポート機能は、ページオブジェクトデザインパターンの考え方に沿ったテストを作成する際に、もっとも省力化できるように設計しました。例えば、ページオブジェクトクラスの自動生成や、画面遷移時/再描画時の自動検証などです。

豆蔵が自動化したこと

#
  1. 画面を操作するためのJavaソースコードの大半を自動生成できる
  2. ブラウザ操作、ポップアップや画面遷移操作を簡易に実装できる
    1. 画面切り替えでWindowハンドルを扱わなくてもいいようにした(開いたウインドウを操作して、閉じたら元のウインドウを操作する)
  3. 任意のタイミングにおける画面の内容(=htmlソース)について、期待値と比較検証できる
    1. 画面全体の比較や、IDを指定したエレメント内容の比較
    2. 毎回変動する要素や比較不要な要素については正規表現で除外して比較
    3. いちいち検証コードを書かなくても勝手に検証するモードも用意(画面描画のタイミング)
  4. 任意のタイミングまでに行われたDB更新内容について、期待値と比較検証できる(今のところoracleだけ)
    1. 登録、更新、削除の内容が記録され、意図しない変更を検出する
    2. 毎回変動するカラムについてはテーブル名と列名を指定することで比較対象外にできる
    3. いちいち検証コードを書かなくても勝手に検証するモードも用意(テスト終了のタイミング)
  5. テストケース実行後に、DBをテスト実行前の状態に戻すことができる(今のところoracleだけ)
    1. 追加、更新、削除してコミットしたデータ、シーケンスの値が元に戻る(テスト終了のタイミング)
  6. ダウンロードしたファイルについて、期待値との比較ができる
    1. テキストとして比較:txt、xls、xlsx、doc、docx
    2. 画像として比較:pdf(差異個所を強調表示したpngを出力)
    3. 展開して中身を比較:zip
    4. いちいち検証コードを書かなくても勝手に検証するモードも用意(ファイル保存のタイミング)
  7. 画面やDB更新の検証に用いる期待値を自動的に生成することができる
    1. テストを実行すると特定のフォルダにファイルを生成する(画面描画、ファイル保存、テスト終了のタイミング)

配置のイメージ

#

(テスト対象とテストが黄緑、テスト支援機能が青)

flowchart TB
    subgraph webServer[Webサーバー]
    webapp[テスト対象Webアプリ]
    style webapp fill:lawngreen
    end
    subgraph dbServer[DBサーバー]
      subgraph oracleInstance[Oracleインスタンス]
        schema1[管理用スキーマ]
        schema2[テスト用スキーマ]
        style schema2 fill:lawngreen
        logMiner[LogMiner]
      end
      redoLog[REDOログ]
      logMiner-->redoLog
      oracleInstance-->redoLog
    end
    subgraph ciServer[CIサーバー]
      subgraph JUnit+Selenium
      testSupport[自動打鍵テスト支援機能]
      style testSupport fill:deepskyblue
      testClass[テストクラス+PageObject]
      style testClass fill:lawngreen
      testClass-->testSupport
      end
      browser[ブラウザー]
      testSupport-->browser
      testSupport-->schema1
      testSupport-->schema2
      testSupport-->logMiner
      schema1-->schema2
      browser-->webapp
      webapp-->schema2
    end

テスト作成時の作業手順

#

その画面に対するテストを初めて作成する場合

#
  • テスト対象アプリを表示してページオブジェクトを自動生成(8割程度自動)
  • 画面を見ながら操作を決めて、JUnitのテストケースを作成(手動)
    • 画面名や要素名は表示されている通りの日本語なので、操作したい内容をそのまま日本語で書くイメージ
    • ページオブジェクトにある画面遷移メソッドの戻り値を変えたり、項目の指定方法を変えたりもします(この画面に対してテストを追加していく場面ではこれが再利用されます)
  • 期待値がない状態で実行してみる
    • 動きに問題がなければ、actualsフォルダーに自動的に作成されたファイル(画面描画毎に自動で出力されたhtmlファイル、終了時に自動で出力されたテスト実行前後のDB差分csvファイル、ダウンロードしたファイル)を、期待値を置くフォルダーに移動する
  • 期待値がある状態で実行する
    • 期待値通りなら終了
    • 差分が出たら内容に応じて微修正する(テストを直すか、無視する設定にする)
  • 特に重要なところは明示的にページオブジェクトにメソッドを作ったり、テストケースで変数を使った操作を作ったりする
    • よくあるのは登録操作でキーになるもの(顧客番号など)が発行・表示され、そのキーを使って検索や参照などの操作を継続するようなケースです。

同じ画面に対して別のテストケースを作成する場合

#
  • ページオブジェクトのメソッドはすでに作りこまれているので一番最初よりもかなり省力化されます

画面に変更があった場合

#
  • レイアウトの変更だけであれば既存のテストコードには影響がありません。ページオブジェクトのメソッドもidやname、textで項目を指定していれば影響ありません。テストを実行すれば操作はそのまま行えるはずです。期待値と差分が出るのでテキスト比較ツールで確認し、想定通りであれば期待値を上書きします。
  • 画面項目に追加があっても入力必須でなければ既存のテストコードには影響がありません。

ロジックを修正して画面やDB更新内容に期待値との差分が検出された場合

#
  • 修正が意図しない画面やテーブルに影響を与えていないかどうか(デグレードしていないかどうか)確認します。想定通りであれば期待値を上書きします。

どういう仕組みになっているのか

#

面白そうなところいくつかについて解説します。

ページオブジェクト自動生成

#

Webページからブックマークレットを呼び出すと、ページのタイトルや操作可能エレメントの情報をテキスト形式の中間ファイルとしてダウンロードできるようにし、ダウンロードした中間ファイルからJavaのクラスを自動生成する仕組みを用意しました。

  1. ページオブジェクトを生成したい画面をブラウザーで開く
  2. 中間ファイル作成用スクリプトレットを実行する
    1. アドレスバーにURLを直接入力するか、ブックマークレットにしておいて実行する
  3. ダウンロードダイアログが出るので保存する

中間ファイルのイメージを示します。

顧客管理メニュー画面
a text 顧客検索 顧客検索
a text 顧客登録 顧客登録
:
顧客検索画面
button id customerSearchButton 顧客検索
text id customerName 顧客名称
text id userNo 担当社員番号
button id userSearchButton 検索
a text 前ページ 前ページ
a text 次ページ 次ページ
a text C0000001 C0000001
a text C0000002 C0000002
:
  • 1行目は画面タイトルです。
  • 2行目以降は画面内の操作可能な要素が1行ごとにタブ区切りで出力されます。
    • 第1項目:画面上の要素の種別(a, text, button, textareaなど)
    • 第2項目:この項目を指定するために指定する属性(idを指定、nameを指定、textを指定など)
    • 第3項目:この項目を指定するために指定する値(第2項目と組み合わせ)
    • 第4項目:画面の要素名(これがそのままメソッド名になります)

中間ファイルからJavaファイルを生成

中間ファイルをJavaファイルに変換する仕組みを用意して、以下のようなファイルを自動生成します。

public class 顧客管理メニュー画面
        extends BasePageObject<顧客管理メニュー画面> {

    @Override
    public String getPageTitle() {
        return "顧客管理メニュー画面";
    }

    public 顧客管理メニュー画面 ___顧客管理メニュー画面___() {
        return this;
    }

    public 顧客管理メニュー画面 顧客検索() {
        click(By.linkText("顧客検索"));
        return this;
    }

    public 顧客管理メニュー画面 顧客登録() {
        click(By.linkText("顧客登録"));
        return this;
    }
}
public class 顧客検索画面
        extends BasePageObject<顧客検索画面> {

    @Override
    public String getPageTitle() {
        return "顧客検索画面";
    }

    public 顧客検索画面 ___顧客検索画面___() {
        return this;
    }

    public 顧客検索画面 顧客検索() {
        click(By.id("customerSearchButton"));
        return this;
    }

    public 顧客検索画面 顧客名称(String value) {
        sendKeys(By.id("customerName"), value);
        return this;
    }

    public 顧客検索画面 担当社員番号(String value) {
        sendKeys(By.id("userNo"), value);
        return this;
    }

    public 顧客検索画面 検索() {
        click(By.id("userSearchButton"));
        return this;
    }

    public 顧客検索画面 前ページ() {
        click(By.linkText("前ページ"));
        return this;
    }

    public 顧客検索画面 次ページ() {
        click(By.linkText("次ページ"));
        return this;
    }

    public 顧客検索画面 C0000001() {
        click(By.linkText("C0000001"));
        return this;
    }

    public 顧客検索画面 C0000002() {
        click(By.linkText("C0000002"));
        return this;
    }
}

必要に応じて、画面遷移する部分の戻り値を別のページオブジェクトに変更したり、可変部分についてパラメーター化したりするなど、修正します。

自動検証

#

普通に画面操作をして画面描画する際や、画面遷移する際、ポップアップを開く際などに勝手に検証を行うようにしたので、いちいちアサーションを入れなくてもいいようにしました(ページオブジェクトを生成するメソッドを用意し、その中でやるようにしました)。
(テスト対象とテストが黄緑、テスト支援機能が青)

クラス図
シーケンス図

ファイルのダウンロードも同様で、ファイルを保存したら勝手に期待値と比較し、差があったらテストの最後にエラーになるようにしました。
テストを実行すると、自動検証の際にわかりやすい名前でhtmlファイルやダウンロードファイルがactualsフォルダーに自動生成されます。それらをそのまま期待値のフォルダーに上書きすればいいので、期待値づくりの手間はかかりません。
ただし、最初の1回だけは自動で操作される様子を目視する必要があります(テスト実装時に目視するでしょうが)。

DB更新内容の比較

#

OracleのLogMinerを使ってREDOログを採掘し、変更のタイプ(INSERT、UPDATE、DELETE)、変更内容を取得します。
そして表ごとに変更された行のデータをCSVファイルに出力すると、テストの操作でDBに行われた変更が一目でわかります。

以前実行したときのファイルと比較することで、DB更新に差が出ていないかどうか検証します。
検証漏れを防ぐため、基本的にはテストケース終了のタイミングで勝手に検証を行うようにしました。

いくつか困るケースがあるので以下のように対処します。

  • 毎回差分になるケース
    • シーケンスの値
      • テスト実施時に設定したいシーケンスの値をあらかじめ(共通機能用に用意しておいたテーブルに)保持しておきます。テスト開始前に現在のシーケンスの値を一括で変更し、テスト終了後に戻します。
      • 例えばテストデータ作成時にはシーケンスを1などの初期値に設定しておき、テスト実施時には100001などに変更してから実施すると、テストで使用されるシーケンスの値は毎回同じになります。テスト実施後には元に戻すことで、テストデータの追加などでシーケンス値が重複しないようにします。
    • 日付や時刻
      • これはテスト対象アプリで対処しておくとスマートです。アプリで日付や時刻を利用する際に特定のコンポーネントから取得するようにしておき、テスト用環境にデプロイする際にはそのコンポーネントを固定の日付や時刻を返却するようなモックに差し替えます。
    • 上記対処をしても差分になるもの
      • 順不同なコレクションを永続化した場合のサロゲートキーや、モック化していないランダム文字列の生成機能などがある場合は深追いせず諦めています。
      • DB差分の検証時に無視するように、テーブル名+カラム名をテスト用の設定ファイルなどに記載します。記載されたカラムはDB更新の検出ファイルに出力しないようにします。

テストで更新されたDBの高速なリストア

#

DBスキーマ全体をリストアすると時間がかかるので、OracleのLogMinerを使ったり、LogMinerが対応していないところ(シーケンスとかLOBの値を戻すところ)は自前でテスト前の状態をとっておいてテスト後に戻すようにしたりして、変更があった分だけ戻すようにしました。これはかなり高速になりました。

テストしやすいアプリにしよう

#

これまでの経験で、Webアプリの方も少しだけ気をつけて作っておくとテストしやすいアプリになることがわかりました。
最低限こうしておくといいというポイントをあげておきます。

  • 日付や時刻をテスト用に固定化できるようにしておく
    • 日付や日時取得のコンポーネントを作っておいて、差し替えられるようにしておく
  • 操作可能エレメントにわかりやすいIDかNameをつけておく
    • IDやNameなどがないと(xpathなどで特定するような場合)、画面デザインの変更でページオブジェクトの修正が必要になります
  • ラベルにも(Keyになるものなど)大事なものにはIDをつけておく
    • 顧客番号や契約番号などを発行するような画面では、ラベルにIDをつけておくと、その番号を取得して次の操作を行うテストが書きやすくなります

最後に

#

ここまで仕組みを整えておくと、機能追加や機能変更をリリースする際に、意図しないうちに既存機能に影響を与えてしまっていないかという確認に機械的なお墨付きを与えられるため、実装者にもリリース判定者にもかなりの安心を与えられます。
テストと検証の自動化は工数削減効果も大きいのですが、以下のような人的ミスが発生するリスクを排除するというところにも価値があると感じます。
(しかも人的リスクはテストを行うたびに発生する可能性があります。)

  • 文章で書かれたテストシナリオをテスターが読む際に誤解するリスク。
  • 操作結果の画面キャプチャーを紙芝居のように保存する際に必要な画面キャプチャーを取り忘れるリスク、別の画面キャプチャーを保存してしまうリスク。
  • テスト結果のレビューワーが画面キャプチャーを見て確認する際に期待値を誤解するリスク、見間違う(見逃す)リスク。
  • テスターやレビューワーになる人の能力のばらつきによりバグやデグレを見逃すリスク。
  • そもそもテスト範囲の選定を間違うリスク(テストしなかったところにデグレ場発生するリスク)。

このプロジェクトでは普段の開発保守作業以外にも、OracleやWebLogicなどのミドルウェアをバージョンアップする際に、全機能の動作に差分がないかを確認するという用途でも非常に役に立ちました。
実際にOracleのバグのせいでデータに差分が出ることが分かったり(パッチを当てて解決できました)、他のライブラリーのバージョンアップで動作に違いが出ることを検出するなどの成果も上げています。

この自動化の記事がどこかのプロジェクトの効率化(工数やリスクの削減)につながればとてもうれしいです!

Information

色々と自動化してみましたが、ページオブジェクトの自動生成についてはやらなくても良かったかも知れません。ページオブジェクトの実装は最初の1回ということと、実装内容がとても簡単ということが理由です。

あとは特に書きませんでしたが、Javaのテストコードを見るだけでどの画面に何をしているのか分かるため、テストシナリオの一覧を自動生成するようにしました。以下の項目をエクセルに出力すると、ソースコードが読めない人でもどんなテストをしているのか分かります。

  • テストする内容がわかるように日本語で書かれたテストメソッド名
  • テスト対象画面やその操作がわかるように日本語で書かれた実装コード
  • ファイル名を見ただけで画面名や操作名がわかるようにした保存したHTMLファイル名、ダウンロードファイル名

  1. Seleniumブラウザー自動化プロジェクト ↩︎

  2. Puppeteer ↩︎

  3. Playwright ↩︎

  4. ページオブジェクトモデル ↩︎ ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。