跳到主要内容

MockMvc 与 WebDriver

DeepSeek V3 中英对照 MockMvc and WebDriver

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

为什么选择 WebDriver 和 MockMvc?

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

备注

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

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

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

如果其中一个字段名为“summary”,我们可能会在测试中的多个地方看到类似以下内容的重复:

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

那么,如果我们将 id 改为 smmry 会发生什么?这样做会迫使我们更新所有测试以包含这一更改。这违反了 DRY 原则,因此我们理想情况下应该将这段代码提取到一个单独的方法中,如下所示:

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);
}
java

这样做可以确保如果更改 UI,我们不必更新所有测试。

我们甚至可以更进一步,将这个逻辑封装在一个表示当前 HtmlPageObject 中,如下例所示:

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());
}
}
java

以前,这种模式被称为页面对象模式。虽然我们当然可以使用 HtmlUnit 来实现这一点,但 WebDriver 提供了一些工具,我们将在以下部分中探讨这些工具,以使这种模式更容易实现。

MockMvc 和 WebDriver 配置

要在 MockMvc 中使用 Selenium WebDriver,请确保你的项目中包含对 org.seleniumhq.selenium:selenium-htmlunit3-driver 的测试依赖。

我们可以通过使用 MockMvcHtmlUnitDriverBuilder 轻松创建一个与 MockMvc 集成的 Selenium WebDriver,如下例所示:

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
java
备注

这是一个使用 MockMvcHtmlUnitDriverBuilder 的简单示例。有关更高级的用法,请参阅 高级 MockMvcHtmlUnitDriverBuilder

前面的示例确保任何以 localhost 作为服务器引用的 URL 都会被定向到我们的 MockMvc 实例,而无需建立真实的 HTTP 连接。其他任何 URL 则会像平常一样通过网络连接请求。这使得我们可以轻松测试 CDN 的使用。

MockMvc 和 WebDriver 的使用

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

CreateMessagePage page = CreateMessagePage.to(driver);
java

然后我们可以填写表单并提交以创建消息,如下所示:

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

这通过利用页面对象模式改进了我们的 HtmlUnit 测试 设计。正如我们在 为什么选择 WebDriver 和 MockMvc? 中提到的,我们可以在 HtmlUnit 中使用页面对象模式,但在 WebDriver 中使用起来要容易得多。考虑以下 CreateMessagePage 实现:

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);
}
}
java
  • CreateMessagePage 继承自 AbstractPage。我们不会详细讨论 AbstractPage 的细节,但总的来说,它包含了我们所有页面的通用功能。例如,如果我们的应用程序有一个导航栏、全局错误消息和其他功能,我们可以将这些逻辑放在一个共享的位置。

  • 我们对 HTML 页面中感兴趣的每个部分都有一个成员变量。这些变量的类型是 WebElement。WebDriver 的 PageFactory 让我们可以通过自动解析每个 WebElement 来移除 CreateMessagePage 的 HtmlUnit 版本中的大量代码。PageFactory#initElements(WebDriver,Class<T>) 方法通过使用字段名称并在 HTML 页面中按元素的 idname 查找来自动解析每个 WebElement

  • 我们可以使用 @FindBy 注解 来覆盖默认的查找行为。我们的示例展示了如何使用 @FindBy 注解通过 css 选择器 (input[type=submit]) 查找提交按钮。

最后,我们可以验证新消息是否成功创建。以下断言使用了 AssertJ 断言库:

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

我们可以看到,ViewMessagePage 允许我们与自定义的领域模型进行交互。例如,它暴露了一个返回 Message 对象的方法:

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

然后我们可以在断言中使用这些丰富的领域对象。

最后,我们一定不要忘记在测试完成后关闭 WebDriver 实例,如下所示:

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

有关使用 WebDriver 的更多信息,请参阅 Selenium 的 WebDriver 文档

高级 MockMvcHtmlUnitDriverBuilder

在到目前为止的示例中,我们以最简单的方式使用了 MockMvcHtmlUnitDriverBuilder,即基于 Spring TestContext Framework 为我们加载的 WebApplicationContext 来构建 WebDriver。这种方法在这里重复如下:

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
java

我们还可以指定额外的配置选项,如下所示:

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();
}
java

作为替代方案,我们可以通过单独配置 MockMvc 实例并将其提供给 MockMvcHtmlUnitDriverBuilder 来实现完全相同的设置,如下所示:

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();
java

虽然这种方式更为冗长,但通过使用 MockMvc 实例构建 WebDriver,我们可以充分利用 MockMvc 的强大功能。

提示

有关创建 MockMvc 实例的更多信息,请参阅 配置 MockMvc