MockMvc and WebDriver
在以前的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API 结合使用。在本部分中,我们在 Selenium WebDriver中使用其他抽象以使事情变得更加容易。
Why WebDriver and MockMvc?
我们已经可以使用 HtmlUnit 和 MockMvc,那么我们为什么要使用 WebDriver?Selenium WebDriver 提供了一个非常优雅的 API,使我们可以轻松组织代码。为了更好地展示其工作原理,我们在本节中探讨了一个示例。
尽管是 Selenium 的一部分,但 WebDriver 不需要 Selenium Server 来运行你的测试。 |
假设我们需要确保正确创建消息。测试涉及查找 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)
}
这样做可确保在更改用户界面时,我们不必更新所有测试。
我们甚至可以更进一步,将此逻辑放置在表示我们当前所处“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()
}
}
}
以前,此模式被称为 Page Object Pattern。虽然我们当然可以通过 HtmlUnit 执行此操作,但 WebDriver 提供了一些我们将在以下部分中探索的工具,以使此模式更容易实现。
MockMvc and WebDriver Setup
要将 Selenium WebDriver 与 MockMvc 配合使用,请确保项目包含对 org.seleniumhq.selenium:selenium-htmlunit3-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()
}
这是一个使用 |
上一个示例确保任何将“localhost”引用为服务器的 URL 都定向到我们的 MockMvc 实例,而无需实际的 HTTP 连接。根据正常情况,使用网络连接请求任何其他 URL。这让我们可以轻松测试 CDN 的使用方式。
MockMvc and WebDriver Usage
现在我们可以照常使用 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 test 的设计。正如我们在 Why WebDriver and MockMvc? 中提到的,我们可以在 HtmlUnit 中使用页面对象模式,但这在 WebDriver 中更简单。请考虑以下 CreateMessagePage
实现:
- Java
-
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 可让我们从 CreateMessagePage`的 HtmlUnit 版本中移除许多代码,方法是自动解析每个 `WebElement 。 PageFactory#initElements(WebDriver,Class<T>) 方法使用字段名称自动解析每个 WebElement ,并通过 HTML 页面元素的 `id`或 `name`来查找它。 |
3 | 我们可以使用 @FindBy annotation 来覆盖默认的查找行为。我们的示例展示了如何使用 @FindBy 注解来通过 css 选择器 (input[type=submit] ) 查找我们的提交按钮。
|
4 | CreateMessagePage`扩展了 `AbstractPage 。我们不去讨论 `AbstractPage`的细节,但总结一下,它包含了所有页面公用的功能。例如,如果我们的应用程序有一个导航栏、全局错误消息和其他功能,我们可以将此逻辑放在一个共享的位置。 |
5 | 我们有一个成员变量,用于表示我们感兴趣的 HTML 页面的每个部分。它们是 WebElement`类型。WebDriver 的 `PageFactory 可让我们从 CreateMessagePage`的 HtmlUnit 版本中移除许多代码,方法是自动解析每个 `WebElement 。 PageFactory#initElements(WebDriver,Class<T>) 方法使用字段名称自动解析每个 WebElement ,并通过 HTML 页面元素的 `id`或 `name`来查找它。 |
6 | 我们可以使用 @FindBy annotation 来覆盖默认的查找行为。我们的示例展示了如何使用 @FindBy 注解来通过 css 选择器 (input[type=submit]) 查找我们的提交按钮。 |
最后,我们可以验证是否已成功创建新消息。以下断言使用https://assertj.github.io/doc[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 documentation 。
Advanced MockMvcHtmlUnitDriverBuilder
到目前为止,我们在示例中已以最简单的方式使用 MockMvcHtmlUnitDriverBuilder
,方法是基于 Spring TestContext 框架为我们加载的 WebApplicationContext
构建 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()
}
我们还可以指定其他配置选项,如下所示:
-
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 的所有功能。
有关创建 |