MockMvc + HtmlUnitでE2Eテストをブラウザ・APサーバレスに！

今回はWebアプリケーションのE2Eテストについてお話します。

E2Eテストを自動化するときにはSeleniumを使うことが多いですが、実行までの準備やテストコードのメンテナンスに思った以上の労力がかかることもしばしば。

Spring TestのMockMVCとHTMLUnitを組み合わせることで、ブラウザ・APサーバレスに効率的なテストの実施が可能です。

## はじめに
### Seleniumを利用したE2Eテストの問題点（大変さ）

* WarファイルのAPサーバへのデプロイが必須
* Seleniumのバージョンに合ったブラウザが必要（＝ブラウザの固定化）
* ブラウザのバージョンに合わせたWebDriverドライバが必要（＝環境準備の必要性）
  ※[WebDriverManager](https://github.com/bonigarcia/webdrivermanager)の導入によって、ドライバ入手の負担は減るかもしれません。
* ブラウザの起動に時間がかかり、テスト実行のオーバーヘッドが大きい
* ブラウザおよびバージョンごとに挙動が違う（＝テストコードカスタマイズの必要性）

MockMvcとHTMLUnitを利用することで、これらの問題から解放されコーディングに集中することができます！

### MockMvcとは？

MockMVCとは、Spring Testで提供されており、Spring MVCのWebアプリケーションをモック化してコントローラのテストを行う仕組みです。

コントローラをスタンドアロンで動作させる方法と、アプリケーションコンテキストを読み込んでWebアプリケーションとして動作させる方法がありますが、今回は後者を使用します。

### HtmlUnitとは？

HtmlUnitとは、ブラウザをシミュレートしてHTMLのテストを行う仕組みで、JavaScriptの挙動までテストすることが可能です。

ブラウザをシミュレートするためのインターフェイスとして、HtmlUnit独自のWebClientが提供されていますが、今回はSelemium WebDriverと組み合わせてWebDriverを使用します。

### 今回利用したライブラリのバージョン

* Spring IO Platform Athens-SR2 で管理されるバージョン

## MockMVC + HTMLUnitのセットアップ

3ステップで完了です。

### Maven POMのセットアップ
まずは依存関係をセットアップします。

```xml
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <scope>test</scope>
</dependency>
```
| No. | Description |
|-----|--------------------------------------------------------------------------------|
| (1) | MockMvcを利用するため、`spring-test`を追加します。 |
| (2) | MockMvcでSpring Securityを利用するときは、`spring-security-test`を追加します。 |
| (3) | HtmlUnitとSelenium WebDriverを利用するため、`selenium-java`を追加します。 |

### MockMvcのセットアップ
次に、テストコードでMockMvcを使うためのセットアップを行います。

```java
@RunWith(SpringJUnit4ClassRunner.class) // (1)
@WebAppConfiguration // (2)
@ContextHierarchy({ // (3)
        @ContextConfiguration({"classpath*:web-application-context.xml",
                "classpath*:spring-security-context.xml" }),
        @ContextConfiguration("classpath*:servlet-context.xml") })
public class MockMvcHtmlUnitTest {

    @Autowired
    private WebApplicationContext context; // (4)

    @Before
    public void setup() {
        MockMvc mvc = MockMvcBuilders.webAppContextSetup(context) // (5)
                .addFilters(new CharacterEncodingFilter("UTF-8", true)) // (6)
                .apply(springSecurity()) // (7)
                .alwaysDo(log()) // (8)
                .build(); // (9)
    }
}
```
| No. | Description |
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | テストケースでSpring Testの機能を有効にするため、`SpringJUnit4ClassRunner`を使用します。 |
| (2) | `@WebAppConfiguration`を付与することで、テストケースに`WebApplicationContext`をインジェクションすることができるようになります。 |
| (3) | `@ContextConfiguration`で、アプリケーションが読み込むコンテキストファイルを指定します。 `@ContextConfiguration`には複数のファイルを指定できますが、すべて一つのコンテキストとして展開されます。 コンテキストを階層化したいときは、`@ContextHierarchy`を使用します。 |
| (4) | MockMvcをセットアップするため、`WebApplicationContext`をインジェクションします。 |
| (5) | `MockMvcBuilders#webAppContextSetup()`で、Webアプリケーションとして動作させたコントローラにアクセスするMockMvcをセットアップすることができます。 |
| (6) | さらに`addFilters()`で、サーブレットフィルタを追加することができます。（web.xmlでのセットアップを再現することができます。） |
| (7) | さらに`apply(springSecurity())`で、Spring Securityを有効にします。 `springSecurity()`はデフォルトでSpring SecurityのBean`springSecurityFilterChain`を読み込んでくれますが、異なるBeanを読み込みたいおときは引数でセットすることもできます。 テスト内容によっては、`@MockUser`を併用したり、Spring Securityを無効にするなど柔軟に対応すると良いですね。 |
| (8) | さらに`alwaysDo(log())`でModelや返却されるViewのロギングを有効にします。 `log()`と同様の働きをする`print()`もありますが、`print()`が常に標準出力にロギングするのに対して、`log()`はロガーの設定に応じてロギングする/しないや出力先を変更することができるので、オススメです。 なお、MockMvcのロギングはログカテゴリ`org.springframework.test.web.servlet.result`で行われます。 |
| (9) | 最後に`build()`でMockMvcインスタンスを生成します。 |

※ちなみに、、、Spring Bootなら、以下のようにシンプルにセットアップできます。

```java
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcHtmlUnitTest {
```

### HtmlUnitのセットアップ
最後に、テストコードでHtmlUnitとSelenium WebDriverを組み合わせたHtmlUnitDriverを使うためのセットアップを行います。

```java
    private WebDriver driver;

    @Before
    public void setup() {
        MockMvc mvc = // omitted configure MockMvc.
        driver = MockMvcHtmlUnitDriverBuilder.mockMvcSetup(mvc) // (1)
                .contextPath("/sample-app") // (2)
                .build(); // (3)
    }
```
| No. | Description |
|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | `MockMvcHtmlUnitDriverBuilder#mockMvcSetup()`で、HtmlUnitのWebDriverからMockMvcを経由してリクエストを送信することができるようになります。 |
| (2) | さらに`contextPath()`で、モック化したアプリケーションのコンテキストパスを指定することができます。 MockMvcを使用するだけなら必要ありませんが、実際のアプリケーションと条件を揃えるため、指定しておくことをオススメします。 これにより、テストコードの再利用性が高まります。 |
| (3) | 最後に`build()`でHtmlUnitDriverインスタンスを生成します。 |

## MockMVC + HTMLUnitの使い方

普通にSelenium WebDriverを使用する場合と同様です。
さきほどセットしたコンテキストパスを意識するだけでOK。

```java
    @Test
    public void testController() throws Exception {
        driver.get("http://localhost/sample-app/test-path");
        
        String text = driver.findElement(By.id("text")).getText();
        assertThat(text, is("application is loaded!"));
    }
```

※ちなみに、、、MockMvcだけを利用したときのようにModelやView名を取得することはできません。

## 補足

### 特定のブラウザをシミュレートしたいとき

HtmlUnitはブラウザバージョンを指定することで、特定のブラウザをシミュレートすることができます。
ブラウザバージョンを指定するときはHtmlUnitのセットアップを以下のように変更します。

```java
    @Before
    public void setup() {
        WebClient client = new WebClient(BrowserVersion.CHROME); // (1)
        WebConnectionHtmlUnitDriver driver = new WebConnectionHtmlUnitDriver(BrowserVersion.CHROME); // (2)
        driver.setWebConnection(new MockMvcWebConnection(mvc, client, "/sample-app")); // (3)
    }
```
| No. | Description |
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| (1) | `MockMvcWebConnection`を利用するため、`WebClient`インスタンスを生成します。 このとき、引数として`BrowserVersion`を指定します。 |
| (2) | `MockMvcWebConnection`を利用するため、`WebConnectionHtmlUnitDriver`インスタンスを生成します。 ここでも、引数として`BrowerVersion`を指定します。 |
| (3) | `MockMvcHtmlUnitDriverBuilder#mockMvcSetup()`の代わりに、`MockMvcWebConnection`を利用してMockMvcと連携します。 第三引数としてコンテキストパスを指定することができます。 |

## まとめ

今回はMockMvcとHtmlUnitを組み合わせて、ブラウザ・APサーバレスにE2Eテストを行う方法について説明しました。

実際には、ローカル環境でのテストではブラウザ・APサーバレスでも、正式な本番向けのテストでは実際のサポートブラウザ・APサーバを使用することになると思いますが、WebDriverインターフェイスを利用することで本番向けテストへのテストコード再利用性も期待できます。（私はSpringのプロファイル機能で切り替えられるようにして使っています！）

E2Eテストのハードルを低くして、手作業でのテストを無くしていけると良いですね！