Java
Selenium
spring

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

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

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

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

はじめに

Seleniumを利用したE2Eテストの問題点(大変さ)

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

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

MockMvcとは?

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

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

HtmlUnitとは?

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

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

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

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

MockMVC + HTMLUnitのセットアップ

3ステップで完了です。

Maven POMのセットアップ

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

<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を使うためのセットアップを行います。

@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のBeanspringSecurityFilterChainを読み込んでくれますが、異なる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なら、以下のようにシンプルにセットアップできます。

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

HtmlUnitのセットアップ

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

    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。

    @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のセットアップを以下のように変更します。

    @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テストのハードルを低くして、手作業でのテストを無くしていけると良いですね!