今回はWebアプリケーションのE2Eテストについてお話します。
E2Eテストを自動化するときにはSeleniumを使うことが多いですが、実行までの準備やテストコードのメンテナンスに思った以上の労力がかかることもしばしば。
Spring TestのMockMVCとHTMLUnitを組み合わせることで、ブラウザ・APサーバレスに効率的なテストの実施が可能です。
はじめに
Seleniumを利用したE2Eテストの問題点(大変さ)
- WarファイルのAPサーバへのデプロイが必須
- Seleniumのバージョンに合ったブラウザが必要(=ブラウザの固定化)
- ブラウザのバージョンに合わせたWebDriverドライバが必要(=環境準備の必要性)
※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のセットアップ
まずは依存関係をセットアップします。
<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テストのハードルを低くして、手作業でのテストを無くしていけると良いですね!