MockMvc および WebDriver

前のセクションでは、MockMvc を生の HtmlUnit API と組み合わせて使用する方法を見てきました。このセクションでは、Selenium WebDriver (英語) 内で追加の抽象化を使用して、物事をさらに簡単にします。

なぜ WebDriver と MockMvc なのでしょうか?

すでに HtmlUnit と MockMvc を使用できますが、なぜ WebDriver を使用するのでしょうか? Selenium WebDriver は非常にエレガントな API を提供しており、コードを簡単に整理できます。それがどのように機能するかをよりよく示すために、このセクションの例を調べます。

Selenium (英語) の一部であるにもかかわらず、WebDriver はテストを実行するために Selenium Server を必要としません。

メッセージが適切に作成されるようにする必要があるとします。テストには、HTML フォームの入力要素の検索、入力、さまざまなアサーションの作成が含まれます。

エラー状態もテストする必要があるため、このアプローチでは多数の個別のテストが行われます。例: フォームの一部のみを入力した場合にエラーが発生するようにします。フォーム全体に入力すると、新しく作成されたメッセージが後で表示されます。

フィールドの 1 つに "summary" という名前が付けられている場合、テスト内の複数の場所で次のようなものが繰り返される可能性があります。

  • Java

  • Kotlin

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)

id を smmry に変更するとどうなるでしょうか? これを行うと、すべてのテストを更新してこの変更を組み込むように強制されます。これは DRY の原則に違反するため、次のように、理想的にはこのコードを独自のメソッドに抽出する必要があります。

  • Java

  • Kotlin

public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
	setSummary(currentPage, summary);
	// ...
}

public void setSummary(HtmlPage currentPage, String summary) {
	HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
	summaryInput.setValueAttribute(summary);
}
fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
	setSummary(currentPage, summary);
	// ...
}

fun setSummary(currentPage:HtmlPage , summary: String) {
	val summaryInput = currentPage.getHtmlElementById("summary")
	summaryInput.setValueAttribute(summary)
}

そうすることで、UI を変更した場合にすべてのテストを更新する必要がなくなります。

次の例に示すように、これをさらに一歩進め、現在の HtmlPage を表す Object 内にこのロジックを配置することもできます。

  • Java

  • Kotlin

public class CreateMessagePage {

	final HtmlPage currentPage;

	final HtmlTextInput summaryInput;

	final HtmlSubmitInput submit;

	public CreateMessagePage(HtmlPage currentPage) {
		this.currentPage = currentPage;
		this.summaryInput = currentPage.getHtmlElementById("summary");
		this.submit = currentPage.getHtmlElementById("submit");
	}

	public <T> T createMessage(String summary, String text) throws Exception {
		setSummary(summary);

		HtmlPage result = submit.click();
		boolean error = CreateMessagePage.at(result);

		return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
	}

	public void setSummary(String summary) throws Exception {
		summaryInput.setValueAttribute(summary);
	}

	public static boolean at(HtmlPage page) {
		return "Create Message".equals(page.getTitleText());
	}
}
	class CreateMessagePage(private val currentPage: HtmlPage) {

		val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")

		val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")

		fun <T> createMessage(summary: String, text: String): T {
			setSummary(summary)

			val result = submit.click()
			val error = at(result)

			return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
		}

		fun setSummary(summary: String) {
			summaryInput.setValueAttribute(summary)
		}

		fun at(page: HtmlPage): Boolean {
			return "Create Message" == page.getTitleText()
		}
	}
}

以前は、このパターンはページオブジェクトパターン [GitHub] (英語) として知られていました。確かに HtmlUnit でこれを行うことができますが、WebDriver には、このパターンを実装しやすくするために次のセクションで検討するいくつかのツールが用意されています。

MockMvc および WebDriver のセットアップ

Spring MVC テストフレームワークで Selenium WebDriver を使用するには、プロジェクトに org.seleniumhq.selenium:selenium-htmlunit-driver へのテスト依存関係が含まれていることを確認してください。

次の例に示すように、MockMvcHtmlUnitDriverBuilder を使用して、MockMvc と統合する Selenium WebDriver を簡単に作成できます。

  • Java

  • Kotlin

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
	driver = MockMvcHtmlUnitDriverBuilder
			.webAppContextSetup(context)
			.build();
}
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
	driver = MockMvcHtmlUnitDriverBuilder
			.webAppContextSetup(context)
			.build()
}
これは、MockMvcHtmlUnitDriverBuilder を使用する簡単な例です。より高度な使用箇所については、高度な MockMvcHtmlUnitDriverBuilder を参照してください。

上記の例では、サーバーとして localhost を参照する URL が、実際の HTTP 接続を必要とせずに MockMvc インスタンスにリダイレクトされるようにします。他の URL は、通常どおりネットワーク接続を使用してリクエストされます。これにより、CDN の使用を簡単にテストできます。

MockMvc および WebDriver の使用箇所

これで、通常どおり WebDriver を使用できますが、アプリケーションをサーブレットコンテナーにデプロイする必要はありません。例: 次のメッセージを作成するためにビューをリクエストできます:

  • Java

  • Kotlin

CreateMessagePage page = CreateMessagePage.to(driver);
val page = CreateMessagePage.to(driver)

次に、フォームに入力して送信し、次のようにメッセージを作成します。

  • Java

  • Kotlin

ViewMessagePage viewMessagePage =
		page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
val viewMessagePage =
	page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)

これにより、ページオブジェクトパターンを活用することにより、HtmlUnit テストの設計が改善されます。なぜ WebDriver と MockMvc なのでしょうか? で説明したように、HtmlUnit でページオブジェクトパターンを使用できますが、WebDriver でははるかに簡単です。次の CreateMessagePage 実装を検討してください。

  • Java

  • Kotlin

public class CreateMessagePage extends AbstractPage { (1)

	(2)
	private WebElement summary;
	private WebElement text;

	@FindBy(css = "input[type=submit]") (3)
	private WebElement submit;

	public CreateMessagePage(WebDriver driver) {
		super(driver);
	}

	public <T> T createMessage(Class<T> resultPage, String summary, String details) {
		this.summary.sendKeys(summary);
		this.text.sendKeys(details);
		this.submit.click();
		return PageFactory.initElements(driver, resultPage);
	}

	public static CreateMessagePage to(WebDriver driver) {
		driver.get("http://localhost:9990/mail/messages/form");
		return PageFactory.initElements(driver, CreateMessagePage.class);
	}
}
1CreateMessagePage は AbstractPage を継承します。AbstractPage の詳細については説明しませんが、要約すると、すべてのページに共通の機能が含まれています。例: アプリケーションにナビゲーションバー、グローバルエラーメッセージ、その他の機能がある場合、このロジックを共有の場所に配置できます。
2 関心のある HTML ページの各部分にメンバー変数があります。これらは WebElement 型です。WebDriver の PageFactory [GitHub] (英語) では、各 WebElement を自動的に解決することにより、HtmlUnit バージョンの CreateMessagePage から多くのコードを削除できます。PageFactory#initElements(WebDriver,Class<T>) (英語) メソッドは、フィールド名を使用し、HTML ページ内の要素の id または name で検索することにより、各 WebElement を自動的に解決します。
3@FindBy アノテーション [GitHub] (英語) を使用して、デフォルトの検索動作をオーバーライドできます。この例では、@FindBy アノテーションを使用して、css セレクター (input[type=submit]) で送信ボタンを検索する方法を示しています。
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1)

	(2)
	private lateinit var summary: WebElement
	private lateinit var text: WebElement

	@FindBy(css = "input[type=submit]") (3)
	private lateinit var submit: WebElement

	fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
		this.summary.sendKeys(summary)
		text.sendKeys(details)
		submit.click()
		return PageFactory.initElements(driver, resultPage)
	}
	companion object {
		fun to(driver: WebDriver): CreateMessagePage {
			driver.get("http://localhost:9990/mail/messages/form")
			return PageFactory.initElements(driver, CreateMessagePage::class.java)
		}
	}
}
1CreateMessagePage は AbstractPage を継承します。AbstractPage の詳細については説明しませんが、要約すると、すべてのページに共通の機能が含まれています。例: アプリケーションにナビゲーションバー、グローバルエラーメッセージ、その他の機能がある場合、このロジックを共有の場所に配置できます。
2 関心のある HTML ページの各部分にメンバー変数があります。これらは WebElement 型です。WebDriver の PageFactory [GitHub] (英語) では、各 WebElement を自動的に解決することにより、HtmlUnit バージョンの CreateMessagePage から多くのコードを削除できます。PageFactory#initElements(WebDriver,Class<T>) (英語) メソッドは、フィールド名を使用し、HTML ページ内の要素の id または name で検索することにより、各 WebElement を自動的に解決します。
3@FindBy アノテーション [GitHub] (英語) を使用して、デフォルトのルックアップ動作をオーバーライドできます。この例では、@FindBy アノテーションを使用して、css セレクター(input [type = submit])で送信ボタンを検索する方法を示します。

最後に、新しいメッセージが正常に作成されたことを確認できます。以下のアサーションは、AssertJ (英語) アサーションライブラリを使用します。

  • Java

  • Kotlin

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")

ViewMessagePage を使用すると、カスタムドメインモデルとやり取りできることがわかります。例: Message オブジェクトを返すメソッドを公開します:

  • Java

  • Kotlin

public Message getMessage() throws ParseException {
	Message message = new Message();
	message.setId(getId());
	message.setCreated(getCreated());
	message.setSummary(getSummary());
	message.setText(getText());
	return message;
}
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())

その後、アサーションでリッチドメインオブジェクトを使用できます。

最後に、テストが完了したら、次のように WebDriver インスタンスを閉じることを忘れないでください。

  • Java

  • Kotlin

@AfterEach
void destroy() {
	if (driver != null) {
		driver.close();
	}
}
@AfterEach
fun destroy() {
	if (driver != null) {
		driver.close()
	}
}

WebDriver の使用に関する追加情報については、Selenium WebDriver ドキュメント [GitHub] (英語) を参照してください。

高度な MockMvcHtmlUnitDriverBuilder

これまでの例では、Spring TestContext フレームワークによってロードされた WebApplicationContext に基づいて WebDriver を構築することにより、可能な限り最も簡単な方法で MockMvcHtmlUnitDriverBuilder を使用しました。このアプローチは、次のようにここで繰り返されます。

  • Java

  • Kotlin

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
	driver = MockMvcHtmlUnitDriverBuilder
			.webAppContextSetup(context)
			.build();
}
lateinit var driver: WebDriver

@BeforeEach
fun setup(context: WebApplicationContext) {
	driver = MockMvcHtmlUnitDriverBuilder
			.webAppContextSetup(context)
			.build()
}

次のように、追加の構成オプションを指定することもできます。

  • Java

  • Kotlin

WebDriver driver;

@BeforeEach
void setup() {
	driver = MockMvcHtmlUnitDriverBuilder
			// demonstrates applying a MockMvcConfigurer (Spring Security)
			.webAppContextSetup(context, springSecurity())
			// for illustration only - defaults to ""
			.contextPath("")
			// By default MockMvc is used for localhost only;
			// the following will use MockMvc for example.com and example.org as well
			.useMockMvcForHosts("example.com","example.org")
			.build();
}
lateinit var driver: WebDriver

@BeforeEach
fun setup() {
	driver = MockMvcHtmlUnitDriverBuilder
			// demonstrates applying a MockMvcConfigurer (Spring Security)
			.webAppContextSetup(context, springSecurity())
			// for illustration only - defaults to ""
			.contextPath("")
			// By default MockMvc is used for localhost only;
			// the following will use MockMvc for example.com and example.org as well
			.useMockMvcForHosts("example.com","example.org")
			.build()
}

別の方法として、次のように MockMvc インスタンスを個別に構成して MockMvcHtmlUnitDriverBuilder に提供することにより、まったく同じセットアップを実行できます。

  • Java

  • Kotlin

MockMvc mockMvc = MockMvcBuilders
		.webAppContextSetup(context)
		.apply(springSecurity())
		.build();

driver = MockMvcHtmlUnitDriverBuilder
		.mockMvcSetup(mockMvc)
		// for illustration only - defaults to ""
		.contextPath("")
		// By default MockMvc is used for localhost only;
		// the following will use MockMvc for example.com and example.org as well
		.useMockMvcForHosts("example.com","example.org")
		.build();
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed

これはより冗長ですが、MockMvc インスタンスを使用して WebDriver を構築することにより、指先で MockMvc の全機能を使用できます。

MockMvc インスタンスの作成に関する追加情報については、セットアップの選択を参照してください。