MockMvc和WebDriver

在前几节中,我们已经看到了如何在原始HtmlUnit API中使用MockMvc。在本节中,我们将在Selenium WebDriver中使用额外的抽象来使事情变得更加简单。

为什么要使用WebDriver和MockMvc?

我们已经可以使用HtmlUnit和MockMvc,那么为什么我们要使用WebDriver呢?Selenium WebDriver提供了一个非常优雅的API,让我们可以轻松地组织我们的代码。为了更好地展示它的工作原理,我们在本节中探讨一个示例。

尽管是Selenium的一部分,但WebDriver不需要Selenium服务器来运行您的测试。

假设我们需要确保消息被正确创建。测试涉及查找HTML表单输入元素,填写它们,并进行各种断言。

这种方法会导致许多单独的测试,因为我们还想测试错误条件。例如,我们希望确保如果只填写表单的一部分,我们会收到一个错误。如果填写整个表单,新创建的消息应该在之后显示。

如果其中一个字段被命名为“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时更新所有测试。

我们甚至可以进一步将这个逻辑放在代表我们当前所在的HtmlPageObject中,如下面的示例所示:

  • 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()
		}
	}
}

以前,这种模式被称为页面对象模式。虽然我们当然可以在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都会被重定向到我们的MockMvc实例,而无需真正的HTTP连接。任何其他URL都将通过网络连接请求,与正常情况下一样。这让我们可以轻松测试CDN的使用。

MockMvc和WebDriver的使用

现在我们可以像平常一样使用WebDriver,但无需将应用部署到Servlet容器中。例如,我们可以请求视图创建消息如下:

  • 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);
	}
}
1 CreateMessagePage扩展了AbstractPage。我们不详细介绍AbstractPage的细节,但总的来说,它包含了所有页面的通用功能。例如,如果我们的应用程序有导航栏、全局错误消息和其他功能,我们可以将这些逻辑放在一个共享位置。
2 我们为我们感兴趣的HTML页面的每个部分都有一个成员变量。这些变量的类型为WebElement。WebDriver的PageFactory让我们可以通过自动解析每个WebElement来删除HtmlUnit版本的CreateMessagePage中的大量代码。PageFactory#initElements(WebDriver,Class<T>)方法通过使用字段名自动解析每个WebElement,并通过HTML页面中元素的idname查找它。
3 我们可以使用@FindBy注解来覆盖默认的查找行为。我们的示例展示了如何使用@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)
		}
	}
}
1 CreateMessagePage扩展了AbstractPage。我们不详细介绍AbstractPage的细节,但总的来说,它包含了所有页面的通用功能。例如,如果我们的应用程序有导航栏、全局错误消息和其他功能,我们可以将这些逻辑放在一个共享位置。
2 我们为我们感兴趣的HTML页面的每个部分都有一个成员变量。这些变量的类型为WebElement。WebDriver的PageFactory让我们可以通过自动解析每个WebElement来删除HtmlUnit版本的CreateMessagePage中的大量代码。PageFactory#initElements(WebDriver,Class<T>)方法通过使用字段名自动解析每个WebElement,并通过HTML页面中元素的idname查找它。
3 我们可以使用@FindBy注解来覆盖默认的查找行为。我们的示例展示了如何使用@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())

We can then use the rich domain objects in our assertions.

Lastly, we must not forget to close the WebDriver instance when the test is complete, as follows:

  • Java

  • Kotlin

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

For additional information on using WebDriver, see the Selenium WebDriver documentation.

Advanced MockMvcHtmlUnitDriverBuilder

In the examples so far, we have used MockMvcHtmlUnitDriverBuilder in the simplest way possible, by building a WebDriver based on the WebApplicationContext loaded for us by the Spring TestContext Framework. This approach is repeated here, as follows:

  • 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()
}

We can also specify additional configuration options, as follows:

  • 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()
}

As an alternative, we can perform the exact same setup by configuring the MockMvc instance separately and supplying it to the MockMvcHtmlUnitDriverBuilder, as follows:

  • 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

This is more verbose, but, by building the WebDriver with a MockMvc instance, we have the full power of MockMvc at our fingertips.

For additional information on creating a MockMvc instance, see Setup Choices.