SelenideでSeleniumのテスト用サイトをテストしてみる

# はじめに
SeleniumのJavaラッパーであるSelenideを使ってWebサイトの画面テストをしてみます。
例として、「日本Seleniumユーザーコミュニティ」が提供しているテスト用サイトを題材にします。
 テスト用サイト: http://example.selenium.jp/reserveApp_Renewal/
![ReservationPage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1423540/c5b46a25-7bfe-db7d-ebd6-522b8a8a083b.png)


# 環境
- macOS Catalina
- Maven 3.6.3
- JDK 1.8.0_181
- Google Chrome 90.0.4430.93

# Maven設定
Mavenを使って、必要なライブラリ等の準備をします。
Selenideは、JUnitと組み合わせて利用し、テスト結果レポートをAllureを使って出力します。

## pom.xml

```xml:pom.xml
<!-- 省略 -->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <aspectj.version>1.8.10</aspectj.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.codeborne</groupId>
      <artifactId>selenide</artifactId>
      <version>5.20.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.30</version>
    </dependency>
    <dependency>
      <groupId>io.qameta.allure</groupId>
      <artifactId>allure-junit4</artifactId>
      <version>2.13.9</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.qameta.allure</groupId>
      <artifactId>allure-selenide</artifactId>
      <version>2.13.9</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.20</version>
        <configuration>
          <testFailureIgnore>false</testFailureIgnore>
          <argLine>
            -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
          </argLine>
          <properties>
            <property>
              <name>listener</name>
              <value>io.qameta.allure.junit4.AllureJunit4</value>
            </property>
          </properties>
          <systemProperties>
            <property>
              <name>allure.results.directory</name>
              <value>${project.build.directory}/allure-results</value>
            </property>
          </systemProperties>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectj.version}</version>
          </dependency>
        </dependencies>
      </plugin>
      <plugin>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-maven</artifactId>
        <version>2.10.0</version>
      </plugin>
    </plugins>
  </build>
</project>
```

- `pom.xml`の作成は、[Allureのドキュメント](https://docs.qameta.io/allure/#_junit_4)のJUnit 4、Selenide、ReportingのMavenの節を参考にしています
- また、GitHubの[Selenide + Allure + JUnitのサンプルプロジェクト](https://github.com/selenide-examples/selenide-allure-junit)の`pom.xml`の記載を参考に作成しています。

# テスト作成
今回は、PageObjectパターンを利用したテストを作成します。

○参考
- [Page Object Patternで保守性の高いUIテストコードを書こう！【Selenide】](https://non-programmer-lab.com/page-object-pattern/)
- https://selenide.gitbooks.io/user-guide/content/en/pageobjects.html

次のテストシナリオを実現できるように、テスト準備やJUnitのテストを行うクラス1つと3画面分のPageObjectクラスを作成します。

## テストシナリオ

1. ユーザが正しく情報を入力するパターン
  1. 予約情報入力フォームに必要な情報を漏れなく入力し、「利用規約に同意して次へ」をクリック
  1. 予約内容確認画面で「確定」をクリック
  1. 予約完了画面が表示
1. 必須項目の入力が未でエラーとなるパターン
  1. 予約情報入力フォームで「お名前」の項目を入力せずに「利用規約に同意して次へ」をクリック
  1. 予約エラー画面が表示


## テストコード(`SelenideTest.java`)
```java:src/test/java/com/example/SelenideTest.java
package com.example;

import org.junit.*;
import org.junit.runners.MethodSorters;
import org.openqa.selenium.*;
import org.openqa.selenium.support.events.AbstractWebDriverEventListener;
import com.codeborne.selenide.*;
import com.codeborne.selenide.junit.TextReport;
import com.codeborne.selenide.logevents.SelenideLogger;
import io.qameta.allure.Attachment;
import io.qameta.allure.selenide.AllureSelenide;
import java.time.LocalDateTime;
import static com.codeborne.selenide.Condition.*;

@FixMethodOrder (MethodSorters.NAME_ASCENDING)
public class SelenideTest {
	ReservationPage reservationPage = new ReservationPage();
    CheckInfoPage checkInfoPage = new CheckInfoPage();
    FinalConfirmPage finalConfirmPage = new FinalConfirmPage();
	@Rule
	public TextReport report = new TextReport();

	@BeforeClass
	public static void setUp() {
		SelenideLogger.addListener("AllureSelenide", new AllureSelenide().screenshots(true).savePageSource(true));
		// 匿名クラスを使って実装したイベント補足クラスのインスタンスを登録
		WebDriverRunner.addListener(new AbstractWebDriverEventListener() {
			@Override
			// 要素をクリックした直後の処理
			public void afterClickOn(WebElement element, WebDriver driver) {
				screenshot();
			}
			
			@Attachment(type = "image/png")
			// Allureのテストレポートにスクリーンショットを添付
			public byte[] screenshot() {
				return Selenide.screenshot(OutputType.BYTES);
			}
		});
	}

	@Test
	public void test01_OK() {
		checkInfoPage = reservationPage.open()
				.宿泊日を入力(LocalDateTime.now().plusDays(10), 2) // 10日後を指定
				.人数を入力(2)
				.朝食バイキングの有無(false)
				.昼からチェックインプランを選択()
				.お得な観光プランを選択()
				.名前を入力("山田太郎")
				.利用規約に同意して次へ();
		finalConfirmPage = checkInfoPage.予約を確定する();
		finalConfirmPage.タイトルを取得().shouldHave(text("予約を完了しました"));
	}
	@Test
	// 入力必須の名前を入力せずに予約エラーになるパターン
	public void test02_ERROR() {
		checkInfoPage = finalConfirmPage.トップ画面に移動()
				.宿泊日を入力(LocalDateTime.now().plusDays(20), 5)
				.朝食バイキングの有無(true)
				.人数を入力(3)
				.利用規約に同意して次へ();
		
		checkInfoPage.エラーメッセージを取得().shouldHave(text("お名前が指定されていません"));
	}
}
```

- `setUp()`メソッド内で、Allureを使うための準備や要素をクリック直後にスクリーンショットを取得し、テストレポートに添付するための設定を行います。
- Selenideでは、デフォルトでWebDriverManagerが、WebDriverを用意してくれるので、WebDriverの用意はSelenideに任せておきます。利用するブラウザや画面サイズ等の設定を指定する場合は、`com.codeborne.selenide.Configuration`クラス等で設定します。
  - 参考: [Selenideオプション一覧](https://qiita.com/wearemusiclover/items/3fd2b42480df25cc614d)
- `@FixMethodOrder (MethodSorters.NAME_ASCENDING)`のアノテーションは、テストメソッドをメソッド名順で実行するために指定しています。JUnitの本来的な使い方である単体テストでは、実行順は問題にならないかもしれませんが、画面テストでは、順番に画面を遷移させた状態で実行したい場合があります。
  - 参考: [JUnit4でテスト実行順序をメソッド名順にソートする方法](https://program-index.yuuto-kannami.com/junit4-sort)
- PageObjectクラスのメソッド名には、日本語を使っています。


## 予約フォーム画面(`ReservationPage.java`)
以下の予約フォーム画面に対応するPageObjectのクラスを作成します。
![ReservationPage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1423540/6e662149-8ad6-3218-fd1f-682fd0307bbe.png)

```java:src/test/java/com/example/ReservationPage.java
package com.example;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

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

public class ReservationPage {
	public ReservationPage open() {
        Selenide.open("http://example.selenium.jp/reserveApp_Renewal/");
        return this;
    }
	public ReservationPage 宿泊日を入力(LocalDateTime date, int reserveTerm) {
		DateTimeFormatter datetimeformatter = DateTimeFormatter.ofPattern("yyyy/MM/dd/");
		// チェックイン日を設定
		$("#datePick").clear();
		$("#datePick").val(datetimeformatter.format(date)).pressEnter();
		// 宿泊日数を入力
		$("#reserve_term").selectOption(String.valueOf(reserveTerm));
        return this;
    }
	public ReservationPage 人数を入力(int numberOfGuests) {
		$("#headcount").selectOption(String.valueOf(numberOfGuests));
        return this;
    }
	public ReservationPage 朝食バイキングの有無(boolean eatBreakfast) {
		$(byName("bf")).selectRadio(eatBreakfast ? "on" : "off");
        return this;
    }
	public ReservationPage 昼からチェックインプランを選択() {
		$("#plan_a").setSelected(true);
        return this;
    }
	public ReservationPage お得な観光プランを選択() {
		$("#plan_b").setSelected(true);
        return this;
	}
	public ReservationPage 名前を入力(String guestName) {
		$("#guestname").setValue(guestName);
        return this;
    }
    public CheckInfoPage 利用規約に同意して次へ() {
    	$(byText("利用規約に同意して次へ")).click();
        return new CheckInfoPage();
    }
}
```

- テスト用のサイトのためか各要素に丁寧に`id`等が振られている~~という現実にはなかなか見ない画面な~~ので、それを活用して、各項目を操作するメソッドを作成していきます。
- 各メソッドの戻り値は、画面遷移がない場合は自分自身(`this`)を、遷移する場合は遷移先のPageObjectのインスタンスを返すようにします。こうすることで、メソッドチェーンを使って、テストコードを記述することが可能になります。

## 確認画面(`CheckInfoPage.java`)
予約フォームを入力し、「利用規約に同意して次へ」ボタンをクリック後に表示される画面です。
入力不備がある場合は、エラーメッセージが表示されます。
![checkInfo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1423540/e37dc58c-286e-762e-9902-f3d0c4fa6a80.png)

```java:src/test/java/com/example/CheckInfoPage.java
package com.example;

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

public class CheckInfoPage {
	private final SelenideElement errorElement = $("#errorcheck_result");
	private final SelenideElement commitButton = $("button#commit");

    public SelenideElement エラーメッセージを取得() {
        return errorElement;
    }
    public FinalConfirmPage 予約を確定する() {
    	commitButton.click();
        return new FinalConfirmPage();
    }
}
```

- ここでは、シンプルにアサーション用にエラーメッセージを含む要素を取得するためのメソッドと「確定」ボタンをクリックするメソッドを定義しておきます。

## 完了画面(`FinalConfirmPage.java`)
予約完了後に表示される完了画面です。
![finalConfirm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1423540/9be08242-87ae-5e2b-d35e-3938c1fad95b.png)

```java:src/test/java/com/example/FinalConfirmPage.java
package com.example;

import static com.codeborne.selenide.Selectors.byText;
import static com.codeborne.selenide.Selenide.$;
import com.codeborne.selenide.SelenideElement;

public class FinalConfirmPage {
	private final SelenideElement titleElement = $("h1");
	
    public SelenideElement タイトルを取得() {
    	return titleElement;
    }
    public ReservationPage トップ画面に移動() {
    	$(byText("Home")).click();
    	return new ReservationPage();
    }
}
```

- この画面でも、簡単なアサーションをするためにタイトルを取得するためのメソッドを用意しておきます。
- また、右上の「Home」ボタンを押し、予約フォームに戻るためのメソッドも用意しておきます。

# テスト実行/結果レポート
コードが完成したら、テストを実行します。
以下のコマンドを実行するとブラウザが起動し、画面が自動的に操作されていきます。

```console
$ mvn clean test
```

テストが終了したら、以下のコマンドでAllureのテスト結果レポートを見ます。

```console
$ mvn allure:serve
```

実行するとブラウザが起動し、テスト結果レポートが表示されます。
![Allure_Report.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1423540/bf5eb38f-8a8b-a181-f04f-7611ecf58e69.png)


# 参考サイト
- [Selenide](https://selenide.org/index.html)
- [Allure](http://allure.qatools.ru/)
- [日本Seleniumユーザーコミュニティ](http://www.selenium.jp/test-site)
- https://github.com/selenide-examples/selenide-allure-junit