MockMvc 和 WebDriver

在前面的章节中,我们已经看到了如何结合原始 HtmlUnit API 使用 MockMvc。在本节中,我们在 Selenium WebDriver 中使用额外的抽象,让事情变得更加容易。

为什么选择 WebDriver 和 MockMvc?

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

尽管 WebDriver 是 Selenium 的一部分,但它不需要 Selenium Server 来运行你的测试。

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

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

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

  • Java

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

那么如果我们把 id 改成 smmry 会发生什么呢?这样做将迫使我们更新所有测试以包含此更改。这违反了 DRY 原则,所以我们应该理想地将这段代码提取到它自己的方法中,如下所示:

  • Java

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

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

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

  • Java

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

以前,这种模式被称为 Page Object 模式。虽然我们当然可以用 HtmlUnit 做到这一点,但 WebDriver 提供了一些我们在以下部分探索的工具,使这种模式更容易实现。

MockMvc 和 WebDriver 设置

要将 Selenium WebDriver 与 Web MVC Test 框架一起使用,请确保你的项目包含对 org.seleniumhq.selenium:selenium-htmlunit3-driver 的测试依赖。

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

  • Java

WebDriver driver;

@BeforeEach
void setup(WebApplicationContext context) {
  driver = MockMvcHtmlUnitDriverBuilder
      .webAppContextSetup(context)
      .build();
}
这是使用 MockMvcHtmlUnitDriverBuilder 的一个简单示例。有关更高级的用法,请参阅 高级 MockMvcHtmlUnitDriverBuilder

前面的示例确保了任何引用 localhost 作为服务器的 URL 都会被定向到我们的 MockMvc 实例,而无需真正的 HTTP 连接。任何其他 URL 都会像往常一样使用网络连接请求。这让我们能够轻松测试 CDN 的使用。

MockMvc 和 WebDriver 用法

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

  • Java

CreateMessagePage page = CreateMessagePage.to(driver);

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

  • Java

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

这通过利用 Page Object 模式改进了我们的 HtmlUnit 测试 的设计。正如我们在 为什么选择 WebDriver 和 MockMvc? 中提到的,我们可以将 Page Object 模式与 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);
  }
}
1 CreateMessagePage 扩展了 AbstractPage。我们不详细介绍 AbstractPage,但总而言之,它包含我们所有页面的通用功能。例如,如果我们的应用程序有一个导航栏、全局错误消息和其他功能,我们可以将此逻辑放在共享位置。
2 我们为我们感兴趣的 HTML 页面的每个部分都有一个成员变量。这些类型为 WebElement。WebDriver 的 PageFactory 允许我们通过自动解析每个 WebElement 来从 CreateMessagePage 的 HtmlUnit 版本中删除大量代码。https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[PageFactory#initElements(WebDriver,Class<T>)] 方法通过使用字段名称并在 HTML 页面中通过元素的 idname 查找它来自动解析每个 WebElement
3 我们可以使用 @FindBy 注解 来覆盖默认查找行为。我们的示例展示了如何使用 @FindBy 注解通过 css 选择器 (input[type=submit]) 查找我们的提交按钮。

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

+

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

我们可以看到 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;
}

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

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

  • Java

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

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

高级 MockMvcHtmlUnitDriverBuilder

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

  • Java

WebDriver driver;

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

我们还可以指定其他配置选项,如下所示:

WebDriver driver;

@BeforeEach
void setup() {
  driver = MockMvcHtmlUnitDriverBuilder
      // 演示应用 MockMvcConfigurer (Infra Security)
      .webAppContextSetup(context, springSecurity())
      // 仅用于说明 - 默认为 ""
      .contextPath("")
      // 默认情况下 MockMvc 仅用于 localhost;
      // 以下内容也将对 example.com 和 example.org 使用 MockMvc
      .useMockMvcForHosts("example.com","example.org")
      .build();
}

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

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

driver = MockMvcHtmlUnitDriverBuilder
    .mockMvcSetup(mockMvc)
    // 仅用于说明 - 默认为 ""
    .contextPath("")
    // 默认情况下 MockMvc 仅用于 localhost;
    // 以下内容也将对 example.com 和 example.org 使用 MockMvc
    .useMockMvcForHosts("example.com","example.org")
    .build();

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

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