はじめに
クリスマスイブのアドベントカレンダー担当します、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 と組み合わせることにより下記のようなテストケースが自動で検証できるということです。
- フォームのテキストエリアに名前を入力する
- サブミットをする
- サブミット後、アウトプットエリアに入力した名前が出力されることを確認する
Spring Framework 4 からこのライブラリがインテグレートされております。
Introduction to HtmlUnit 参考
HTMLUnit 参考
MockMvc とは
Spring MVC プロジェクトで構築されているアプリケーションの E2E テストを行う上でもう一つ大切な言葉として MockMvc があります。これは Spring Test プロジェクトに含まれる機能であり、Spring MVC フレームワークと結合した状態でテストするために必要不可欠な仕組みです。MockMvc を使用すると、アプリケーションサーバ上にデプロイすることなく Spring MVC の動作を再現できるため、サーバを用意する手間を省くことができ、素早くテストができます。
(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()
}
コントローラの実装
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 テンプレート
<!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>
メッセージ確認テンプレート
<!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>
テストクラスの作成
解説についてはコード中に記載しましたのでそちらのご確認をお願い致します。
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