📄

JUnit+Selenide+POIで打鍵テストと証跡作成を自動化

2022/12/19に公開

ゆるWeb勉強会@札幌 Advent Calendar 2022 19日目の記事です。

前置き

「盆と正月が一緒に来た」なんて言葉がありますが、クリスマスと正月は大体一緒に来ている気がします。DE-TEIUです。
今回はみんな大好き打鍵テストとExcel証跡作成の自動化についての話をします。

打鍵テストと証跡作成が面倒

Webアプリを作成した際、部品ごとにテストコードを作成したり、打鍵テスト(≒アプリを実際にブラウザで動かしてテストすること)を実施したりすると思います。
そして時には、打鍵テストを実行した証跡として、こんな感じで画面のスクリーンショットをペタペタ貼ったExcelファイルを作ることもあるでしょう。

(証跡をこの方式で作成することの是非については一旦置いておきます)

ただ、この打鍵テストとExcel証跡作成作業、正直かなり面倒な作業ではないでしょうか。
そして実施するのが1回だけで済むならまだしも、大抵は「実施の手順を間違えた」とか「テスト進行不可能な不具合が見つかったから後で最初からやり直し」
といった問題が発生し、複数回やらざるを得ない状況になりがちです。そうなると更に辛いですね。

というわけで、どうにかして打鍵テスト(あとついでにExcel証跡作成)を自動化できないか、と思って色々試してみました。

成果物

以下の動画をご覧ください。
https://youtu.be/8so1r7BDXmA

ということで、打鍵テストの実行とExcel証跡の作成がなんとなく自動化できました。

前提条件

  • 使用言語はJava
  • IDEはEclipseを使用
  • Spring Bootで作成したWebアプリのテストを行う

解説

使用したフレームワーク・ライブラリ

JUnit

Javaで作成されたコードの単体テストを自動化するためのフレームワークです。
例えば、こんな雰囲気のテストクラスを作成して実行すれば、任意の機能の呼び出しと、実行結果の検証ができます。

class UserServiceTest {

  @Test
  void SearchUserTest() throws Exception {
    //テストメソッドの上には @Test アノテーションをつける

    //テストしたい処理を実行
    List<User> users = userService.search();

    //assert~メソッドで、実行結果が期待値と同一であるかどうかを検証する
    assertEquals(3, users.size());
  }
}

長くなってしまうため、詳細な使い方については、この記事では割愛します。
JUnitではこんな感じでテストコードが書けるんだというイメージだけ持っていただければ大丈夫です。

Selenide

Selenideとは、Selenium WebDriver(ブラウザ操作自動化フレームワーク)を内蔵したフレームワークです。
WebDriver経由でブラウザを操作するコードが、より簡潔に書けるようになっています。
例えば、以下のような処理を呼び出せば、localhost:8080で実行しているWebアプリでのログインが自動実行されます。

import static com.codeborne.selenide.Selenide.*;

class LoginControllerTest {
  void doLogin() {
    Configuration.browser = "chrome";
    Configuration.baseUrl = "http://localhost:8080";

    //ログイン画面を表示
    open("/");
    //表示した画面のスクリーンショットを取得
    screenShot();
    //ログイン情報を入力
    $("input[name=username]").setValue("yu-za-01");
    $("input[name=password]").setValue("pasuwa-do");
    //ログイン実行
    $("form").submit();
  }
}

Apache POI

Apache POIとは、JavaアプリケーションからMicrosoft ExcelやWordを読み書きできるライブラリです。
(Wordを読み書きしているケースはほぼ見かけないですが)

  1. 既存のExcelファイルを読みこんでWorkbookというクラスのインスタンスを作成
  2. WorkbookからSheetクラスのインスタンスを作成
  3. Sheetの行・列を指定して任意の値を出力

みたいな流れで使えます。(実装例は後述)

実装

  • JUnitでテストコードの実行環境を用意
  • テストコードの中でSelenideを呼び出し、ブラウザを自動操作
  • Selenideでスクリーンショットを取る
  • Apache POIでスクリーンショットを貼り付けたExcelを作成

これら処理が実現できれば、打鍵テスト、Excel証跡作成の自動化ができそうです。

まずは適当にSelenideの初期化処理。テストクラスの中に @BeforeAll アノテーションを付けたメソッドを作成し、その中で以下のような処理を呼ぶと良いでしょう。
そうするとテストコード実行時、最初に1回だけ実行されます。

  //使用ブラウザを決定
  Configuration.browser = "chrome";
  //基底の接続先を指定
  Configuration.baseUrl = "http://localhost:8080";
  Configuration.remote = null;
  Configuration.timeout = 1000L;
  //スクリーンショットの保存先を指定
  //(この場合、プロジェクトのルート直下/report/logintest になる)
  Configuration.reportsFolder = "report/logintest";
  //ブラウザが予期せず閉じた場合に再表示する
  Configuration.reopenBrowserOnFail = true;
  //ブラウザのサイズを指定
  Configuration.browserSize = "1366x768";

次、ログイン処理のテストメソッドはこんな感じです。
(Evidenceクラスは今回独自に作成したものなのでとりあえず無視しても良いです。スクリーンショットのファイルデータとコメントを保持しています)

  @Test
  void doLoginGeneral() throws Exception {
    List<Evidence> evidences = new ArrayList<Evidence>();

    //ログイン画面を表示
    open("/");
    evidences.add(new Evidence(screenshot(OutputType.FILE), "ログイン画面を表示"));

    //ログイン情報を入力
    $("input[name=username]").setValue(USER_GENERAL_MAIL);
    $("input[name=password]").setValue(USER_GENERAL_PASSWORD);
    evidences.add(new Evidence($(".form-wrapper").screenshot(), "ログイン情報を入力"));
    //ログイン実行
    $("form").submit();
    evidences.add(new Evidence(screenshot(OutputType.FILE), "一般ユーザーでログイン完了"));

    //一般ユーザーのマイページに遷移できているかテスト
    assertTrue($(".container").innerText().contains("一般ユーザー"));

    //スクリーンショットを貼り付けたExcelファイルを生成
    createExcelEvidence(evidences, EVIDENCE_FILE_NAME, "1_一般ユーザーログイン");
  }

Selenideで表示中の画面のDOM要素にアクセスしたい場合、$("セレクタ") という書き方を使います。セレクタの書き方はjQueryと大体同じです(たぶん)。
また、screenshotメソッドを呼び出すと表示中の画面のスクリーンショットを撮影し、png形式で保存できます。
$("セレクタ").screenshot() みたいな書き方をすれば、特定のDOM要素だけを切り出してスクリーンショットを撮影できます。

次は作成したスクリーンショットをExcelに貼っていく処理です。
まずはApache POIで既存のExcelファイルを読み込みます。
今回はテンプレート用のExcelファイルをコピーして新規作成するか、既存のExcelファイルを読み込みます。

  private static Workbook initializeWorkbook(File file) throws Exception {
    Workbook workbook = null;
    try {
      //Excelファイルの読み込みを行う
      //ファイルが存在しなかったらテンプレートから作成
      if (file.exists()) {
        workbook = new XSSFWorkbook(new FileInputStream(file));
      } else {
        File template = new File("src/test/resources/template.xlsx");
        workbook = WorkbookFactory.create(new FileInputStream(template));
      }
    } catch (Exception e) {
      e.printStackTrace();
      throw new Exception("Excelファイルの読み込みに失敗しました。");
    }
    return workbook;
  }

読み込んだExcelファイルのテンプレートシートをコピーし、コメントとスクリーンショット画像を出力していきます。

  public static void create(List<Evidence> evidences, String filePath, String sheetName) throws Exception {
    //ワークブック作成
    File file = new File(filePath);
    Workbook workbook = initializeWorkbook(file);

    //テンプレートシートをコピー
    Sheet sheet = workbook.cloneSheet(0);
    workbook.setSheetName(workbook.getSheetIndex(sheet), sheetName);

    //スクリーンショットとコメントを出力
    int currentRow = START_ROW;
    for (int i = 0; i < evidences.size(); i++) {
      Evidence evidence = evidences.get(i);
      putValueToCell(sheet, currentRow, START_COLUMN, evidence.getComment());
      currentRow++;
      try {
        currentRow = putImageToCell(sheet, currentRow, START_COLUMN, evidence.getFile()) + EVIDENCE_ROW_SPACE;
      } catch (Exception e) {
        throw e;
      }
   }

    //ワークブックをファイルに出力
    try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
      workbook.write(fileOutputStream);
    } catch (IOException e) {
      throw e;
    }
  }

セルに文字列を出力。

  private static void putValueToCell(Sheet sheet, int row, int col, String value) {
    Row targetRow = sheet.getRow(row);
    if (targetRow == null) {
      targetRow = sheet.createRow(row);
    }
    Cell targetCell = targetRow.getCell(col);
    if (targetCell == null) {
      targetCell = targetRow.createCell(col);
    }
    targetCell.setCellValue(value);
  }

セルに画像を出力する処理は少し厄介で、 「セルを1つ指定してそこに画像を出力」 ではなく、 「開始セルと終了セルを指定し、その範囲に収まるように画像を出力」 という機能になっています。そのため、セルのサイズによって、画像のサイズ、縦横比が変わってしまうため注意が必要です。

  private static int putImageToCell(Sheet sheet, int row, int col, File imageFile) throws Exception {
    // 画像ファイル読み込み
    InputStream is = new ByteArrayInputStream(FileUtils.readFileToByteArray(imageFile));
    byte[] imageBytes = is.readAllBytes();
    BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
    is.close();
    int imageWidth = image.getWidth();
    int imageHeight = image.getHeight();
    ClientAnchor anchor = sheet.getWorkbook().getCreationHelper().createClientAnchor();
    //画像挿入位置・サイズを指定
    anchor.setCol1(col);
    anchor.setRow1(row);
    int endCol = col + imageWidth / CELL_SIZE;
    int endRow = row + imageHeight / CELL_SIZE;
    anchor.setCol2(endCol);
    anchor.setRow2(endRow);
    //画像のアンカータイプを設定
    anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE);
    //画像を出力
    int index = sheet.getWorkbook().addPicture(imageBytes, Workbook.PICTURE_TYPE_PNG);
    Picture picture = sheet.createDrawingPatriarch().createPicture(anchor, index);
    picture.setLineStyleColor(0, 0, 0);

    return endRow;
  }

画像のサイズ、表示位置を微調整したい場合は、

  anchor.setDy1(anchor.getDy1() + 9525 * 10);
  anchor.setDy2(anchor.getDy2() + 9525 * 10);
  anchor.setDx1(anchor.getDx1() + 9525 * 10);
  anchor.setDx2(anchor.getDx2() + 9525 * 10);

みたいにできます。ちなみにClientAnchorクラスでは、Excel上の1ピクセル=9525 らしいので、上記の例では画像を10pxだけ右下に動かすという処理になります。

まとめ

ということで、

  • JUnitでテストコード作成
  • Selenideでブラウザ自動実行+スクリーンショット撮影
  • Apache POIでExcel証跡作成

がなんとなく実現できました。

他、証跡Excel作成処理に

  • スクリーンショット撮影日時を出力
  • テスト実行前、実行後のDBの各テーブルの中身を出力

あたりも実装できたら更に良さそう。
(そもそもExcel証跡を作らずに済むならそれが一番良い)

ソースコード

GitHubで公開中です。

参考文献

Discussion