Help us understand the problem. What is going on with this article?

Spring Boot と HTMLUnit による E2E テスト入門

はじめに

クリスマスイブのアドベントカレンダー担当します、Mikatus 株式会社の石田です。
アプリケーション開発エンジニアとして Spring Boot 2 でコードをかいてます。

私が担当しているマイクロサービスでは、E2E テストの仕組みがなく、導入したいよね!という話になったため現在進行形で調査を進めています。その過程で学んだことを共有していきたいと思いますので Spring で E2E の仕組みを導入しようと検討されている方は最初の一歩として参考にしてみてください。

まだまだ、Spring 初学者なので不足点、修正箇所がありましたら教えて頂けると嬉しいです。

E2E テストとは

こちら の記事を参照ください。

HTMLUnit とは

HTMLUnit とはレス GUI ブラウザです。HTMLUnit を使うことにより、仮想ブラウザ上で HTML ドキュメントを構造化してくれます。
また、Java でかかれたプログラムから構造化された DOM に対して各種操作(フォームへのデータ入力、リンクのクリック、サブミット)を行うための API も提供しており、Javascript とも親和性も高いライブラリです。

HTMLUnit 単体ではテスト機能は提供されておらず、JUnit などのテスティングフレームワークと組み合わせて E2E テストフェーズで使われることが多いそうです。シュミレートできるブラウザは Chrome, Firefox, Internet Explorer です。

少し難しいことを書きましたが Spring と組み合わせることにより下記のようなテストケースが自動で検証できるということです。
1. フォームのテキストエリアに名前を入力する
2. サブミットをする
3. サブミット後、アウトプットエリアに入力した名前が出力されることを確認する

Spring Framework 4 からこのライブラリがインテグレートされております。

Introduction to HtmlUnit 参考
HTMLUnit 参考

MockMvc とは

Spring MVC プロジェクトで構築されているアプリケーションの E2E テストを行う上でもう一つ大切な言葉として MockMvc があります。これは Spring Test プロジェクトに含まれる機能であり、Spring MVC フレームワークと結合した状態でテストするために必要不可欠な仕組みです。MockMvc を使用すると、アプリケーションサーバ上にデプロイすることなく Spring MVC の動作を再現できるため、サーバを用意する手間を省くことができ、素早くテストができます。

下記に構成の概略図を掲載します。
スクリーンショット 2019-12-23 23.21.34.png

(1) テストケースメソッドは、DispatcherServletにリクエストするデータ(リクエストパスやリクエストパラメータなど)をセットアップする
(2) MOckMvc は、DispatcherServlet に対して擬似的なリクエストを行う。実際に使われる DispatcherServlet は、テスト用に拡張されている org.springframework.test.web.servlet.TestDispatcherServlet となる
(3) DispatcherServlet( Spring MVC のフレームワーク処理)は、リクエスト内容に一致する Handler ( Controler ) のメソッドを呼び出す
(4) テストケースメソッドは、MockMvc が返却する実行結果を受け取り、実行結果の妥当性を検証する

Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発 引用

本来、production 環境でしたら Tomcat の Coyote, Catarina パッケージ群のクラスがクライアントからのリクエストの一次請けになり、その後 Dispatcer Servlet に処理を移管するのですが、Tomcat の機能が無くなっております。

また、下記サイトでも丁寧に解説してあります。
10.2.4. 単体テストで利用するOSSライブラリの使い方

動作環境

  • Spring Boot 2.2.0

  • JDK 1.8.0_211

シナリオ

以降、実装を行なっていきますが、想定するシナリオとしては メッセージ入力画面にて入力されたテキストがメッセージ確認画面に表示されていることを確認する という想定で実装をします。

実装

build.gradle の構成

下記の構成になっております。

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    compile('org.springframework.boot:spring-boot-starter-actuator')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('org.springframework.boot:spring-boot-devtools')
    compile('org.springframework.boot:spring-boot-starter-cache')
    compile group: 'net.sourceforge.htmlunit', name: 'htmlunit', version: '2.36.0'
    compile group: 'org.w3c.css', name: 'sac', version: '1.3'
}

test {
    useJUnitPlatform()
}

コントローラの実装

MessageController.java
package com.example.demo.web;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping("/message")
public class MessageController {

    // メッセージ入力 Form の表示
    @RequestMapping(value = "/showForm", method = RequestMethod.GET)
    public String showForm() {
        // message-form.html(メッセージ入力画面)を表示
        return "message-form";
    }

    // メッセージ確認画面の表示
    @RequestMapping(value = "/postMessage", method = RequestMethod.POST)
    public String postMessage(@RequestParam("message") final String message, final Model model) {
        // "message" という attributeName に対して post されてきた message の内容を model#addAttribute でマッピング
        model.addAttribute("message", message);
        // message-confirm.html(メッセージ確認画面)を表示
        return "message-confirm";
    }

}

Template ファイルの作成

メッセージ入力 Form テンプレート

message-form.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8" />
</head>

<body>
    <h1>メッセージ入力画面</h1>
    <form action="#" th:action="@{/message/processForm}" method="post">
        Message: <input type="text" value="message" id="message" name="message" />
        <input type="submit" />
    </form>
</body>

</html>

メッセージ確認テンプレート

message-confirm.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8" />
</head>

<body>
    <h1>メッセージ確認画面</h1>
    <span th:text="${message}" id="received"></span>
</body>

</html>

テストクラスの作成

解説についてはコード中に記載しましたのでそちらのご確認をお願い致します。

MessageControllerTest.java
package com.example.demo;

import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.htmlunit.MockMvcWebClientBuilder;
import org.springframework.web.context.WebApplicationContext;

import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(SpringExtension.class) // (1)
@SpringBootTest // (2)
public class MessageControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext; // (3)

    private WebClient webClient; 

    @BeforeEach
    public void setup() {
        webClient = MockMvcWebClientBuilder.webAppContextSetup(webApplicationContext).build(); // (4)
    }

    @Test
    public void givenAMessage_whenSent_thenItShows() throws Exception {
        String text = "Hello world!";
        String url = "http://localhost/message/showForm";

        // メッセージ入力画面( message-form.html )を取得
        HtmlPage page = webClient.getPage(url);

        // メッセージ入力画面の id = "message" の DOM 取得
        HtmlTextInput messageText = page.getHtmlElementById("message");

        // 取得した DOM(今回の場合だと input タグ)に "Hello world!" をセット
        messageText.setValueAttribute(text);

        // メッセージ入力画面の form を取得
        HtmlForm form = page.getForms().get(0);

        // メッセージ入力画面の submit を実行( MessageController#postMessage にリクエスト)
        HtmlSubmitInput submit =
                form.getOneHtmlElementByAttribute("input", "type", "submit");
        HtmlPage newPage = submit.click();

        // メッセージ確認画面( message-confirm.html )の id = "received" の DOM からテキストデータを取得
        String receivedText = newPage.getHtmlElementById("received").getTextContent();

        assertEquals(receivedText, text);
    }

}
  • (1) @ExtendWith(SpringExtension.class) について

SpringExtension.class とは Spring 5 から提供されているクラスであり Spring TestContext Framework を JUnit 5 プラットフォーム上で動作できるようにするためのクラス。
また、Spring TestContext Framework とは Spring Test プロジェクトに含まれ、JUnit や TestNG などのテスティングフレームワーク上で Spring が提供しているアノテーション、Java 標準のアノテーション、Spring Test が提供しているアノテーションなど利用することができます。(その他利用できる機能は、こちら を参照)
また、@ExtendWith は JUnit 5 で提供されているアノテーションです。JUnit 5 の機能については こちら で確認してください。

簡単にまとめると Junit 5 プラットフォーム上で Spring で構築された Web アプリケーションをテストする上での各種必要機能を使えるようにするための定義 だと認識しております。

  • (2) @SpringBootTest について

このアノテーションを付与することにより Spring Boot 環境構成でテストを実行することができます。Spring TestContext Framework 構成との違いについては こちら に記載があります。
(私の予想ですが)このアノテーションによりテンプレートエンジンの prefix やエンコードなどが自動的に定義されるようになると考えております。

また、コントローラのみで構成されている場合には @WebMvcTest も使うことができます。このアノテーションの場合、auto scan 対象のクラスが絞られるためテスト実行時の時間を短縮することができます。このアノテーションの詳細な説明は こちら を参考にしてください。

  • (3) WebApplicationContext について

ここでは DI コンテナを以降の(4)で使えるようにするために外から注入している と思ってもらえればいいかと思います。

  • (4) MockMvcWebClientBuilder について

MockMvc に依存する HtmlUnit WebClient インスタンスを生成するためのビルダーです。ここでは、DI コンテナに登録されている各種インスタンスと MockMvc と HtmlUnit WebClient を統合して使えるようにしている と認識しております。以降、このインスタンスを用いてページ取得等々の操作を行なっていきます。

今後について

  • Web API として利用されることを想定した E2E テストの手法を纏める
  • Selenium WebDriver を用いた E2E テストの手法を纏める

参考資料

JUnit5 @RunWith
Class SpringExtension javadoc
Spring 5でSpring Testのここが変わる_公開版
JUnit 5 ユーザーガイド
Spring Web Contexts
E2Eテストの導入から学んだこと
Introduction to HtmlUnit
HtmlUnit
10.2.4. 単体テストで利用するOSSライブラリの使い方
Annotation Type SpringBootTest javadoc
Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発
Spring MVC Test HtmlUnit

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした