MockMvc and WebDriver
在以前的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API 结合使用。在本部分中,我们在 Selenium WebDriver中使用其他抽象以使事情变得更加容易。
In the previous sections, we have seen how to use MockMvc in conjunction with the raw HtmlUnit APIs. In this section, we use additional abstractions within the Selenium WebDriver to make things even easier.
Why WebDriver and MockMvc?
我们已经可以使用 HtmlUnit 和 MockMvc,那么我们为什么要使用 WebDriver?Selenium WebDriver 提供了一个非常优雅的 API,使我们可以轻松组织代码。为了更好地展示其工作原理,我们在本节中探讨了一个示例。
We can already use HtmlUnit and MockMvc, so why would we want to use WebDriver? The Selenium WebDriver provides a very elegant API that lets us easily organize our code. To better show how it works, we explore an example in this section.
尽管是 Selenium 的一部分,但 WebDriver 不需要 Selenium Server 来运行你的测试。 |
Despite being a part of Selenium, WebDriver does not require a Selenium Server to run your tests. |
假设我们需要确保正确创建消息。测试涉及查找 HTML 表单输入元素、填写它们并进行各种断言。
Suppose we need to ensure that a message is created properly. The tests involve finding the HTML form input elements, filling them out, and making various assertions.
这种方法导致了许多单独的测试,因为我们还想测试错误条件。例如,我们希望确保如果我们只填写部分表单,我们会收到错误。如果我们填写整个表单,则新创建的消息应之后显示。
This approach results in numerous separate tests because we want to test error conditions as well. For example, we want to ensure that we get an error if we fill out only part of the form. If we fill out the entire form, the newly created message should be displayed afterwards.
如果其中一个字段的名称为 "`summary`”,我们可能会在测试中的多个地方看到类似以下内容的重复:
If one of the fields were named “summary”, we might have something that resembles the following repeated in multiple places within our tests:
-
Java
-
Kotlin
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
那么,如果我们将 id
更改为 smmry
会发生什么?这样做会迫使我们更新所有测试以纳入此更改。这违反了 DRY 原则,因此我们理想情况下应将此代码提取到其自己的方法中,如下所示:
So what happens if we change the id
to smmry
? Doing so would force us to update all
of our tests to incorporate this change. This violates the DRY principle, so we should
ideally extract this code into its own method, as follows:
-
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)
}
这样做可确保在更改用户界面时,我们不必更新所有测试。
Doing so ensures that we do not have to update all of our tests if we change the UI.
我们甚至可以更进一步,将此逻辑放置在表示我们当前所处“HtmlPage”的“Object”中,如下例所示:
We might even take this a step further and place this logic within an Object
that
represents the HtmlPage
we are currently on, as the following example shows:
-
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 提供了一些我们将在以下部分中探索的工具,以使此模式更容易实现。
Formerly, this pattern was known as the Page Object Pattern. While we can certainly do this with HtmlUnit, WebDriver provides some tools that we explore in the following sections to make this pattern much easier to implement.
MockMvc and WebDriver Setup
要将 Selenium WebDriver 与 MockMvc 配合使用,请确保项目包含对 org.seleniumhq.selenium:selenium-htmlunit3-driver
的测试依赖项。
To use Selenium WebDriver with MockMvc
, make sure that your project includes a test
dependency on org.seleniumhq.selenium:selenium-htmlunit3-driver
.
我们可以通过使用 MockMvcHtmlUnitDriverBuilder
轻松创建与 MockMvc 集成的 Selenium WebDriver,如下例所示:
We can easily create a Selenium WebDriver that integrates with MockMvc by using the
MockMvcHtmlUnitDriverBuilder
as the following example shows:
-
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()
}
这是一个使用 |
This is a simple example of using |
上一个示例确保任何将“localhost”引用为服务器的 URL 都定向到我们的 MockMvc 实例,而无需实际的 HTTP 连接。根据正常情况,使用网络连接请求任何其他 URL。这让我们可以轻松测试 CDN 的使用方式。
The preceding example ensures that any URL that references localhost
as the server is
directed to our MockMvc
instance without the need for a real HTTP connection. Any other
URL is requested by using a network connection, as normal. This lets us easily test the
use of CDNs.
MockMvc and WebDriver Usage
现在我们可以照常使用 WebDriver,而无需将我们的应用程序部署到 Servlet 容器。例如,我们可以请求视图来通过以下内容创建消息:
Now we can use WebDriver as we normally would but without the need to deploy our application to a Servlet container. For example, we can request the view to create a message with the following:
-
Java
-
Kotlin
CreateMessagePage page = CreateMessagePage.to(driver);
val page = CreateMessagePage.to(driver)
然后,我们可以填写表单并提交它来创建消息,如下所示:
We can then fill out the form and submit it to create a message, as follows:
-
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
实现:
This improves on the design of our HtmlUnit test
by leveraging the Page Object Pattern. As we mentioned in
Why WebDriver and MockMvc?, we can use the Page Object Pattern
with HtmlUnit, but it is much easier with WebDriver. Consider the following
CreateMessagePage
implementation:
- 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 extends the AbstractPage . We do not go over the details of
AbstractPage , but, in summary, it contains common functionality for all of our pages.
For example, if our application has a navigational bar, global error messages, and other
features, we can place this logic in a shared location. |
2 | We have a member variable for each of the parts of the HTML page in which we are
interested. These are of type WebElement . WebDriver’s
PageFactory lets us remove a
lot of code from the HtmlUnit version of CreateMessagePage by automatically resolving
each WebElement . The
PageFactory#initElements(WebDriver,Class<T>)
method automatically resolves each WebElement by using the field name and looking it up
by the id or name of the element within the HTML page. |
3 | We can use the
@FindBy annotation
to override the default lookup behavior. Our example shows how to use the @FindBy
annotation to look up our submit button with a css selector (input[type=submit] ).
|
4 | CreateMessagePage extends the AbstractPage . We do not go over the details of
AbstractPage , but, in summary, it contains common functionality for all of our pages.
For example, if our application has a navigational bar, global error messages, and other
features, we can place this logic in a shared location. |
5 | We have a member variable for each of the parts of the HTML page in which we are
interested. These are of type WebElement . WebDriver’s
PageFactory lets us remove a
lot of code from the HtmlUnit version of CreateMessagePage by automatically resolving
each WebElement . The
PageFactory#initElements(WebDriver,Class<T>)
method automatically resolves each WebElement by using the field name and looking it up
by the id or name of the element within the HTML page. |
6 | We can use the
@FindBy annotation
to override the default lookup behavior. Our example shows how to use the @FindBy
annotation to look up our submit button with a css selector (input[type=submit]). |
最后,我们可以验证是否已成功创建新消息。以下断言使用https://assertj.github.io/doc[AssertJ]断言库:
Finally, we can verify that a new message was created successfully. The following assertions use the AssertJ assertion library:
-
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
对象的方法:
We can see that our ViewMessagePage
lets us interact with our custom domain model. For
example, it exposes a method that returns a Message
object:
-
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.
最后,我们一定不要忘记在测试完成后关闭 WebDriver
实例,如下所示:
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()
}
}
有关使用 WebDriver 的更多信息,请参阅 Selenium WebDriver documentation 。
For additional information on using WebDriver, see the Selenium WebDriver documentation.
Advanced MockMvcHtmlUnitDriverBuilder
到目前为止,我们在示例中已以最简单的方式使用 MockMvcHtmlUnitDriverBuilder
,方法是基于 Spring TestContext 框架为我们加载的 WebApplicationContext
构建 WebDriver
。此方法在此重复,如下所示:
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()
}
作为替代方法,我们可以通过单独配置 MockMvc
实例并将其提供给 MockMvcHtmlUnitDriverBuilder
来执行完全相同的设置,如下所示:
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
这更加冗长,但是,通过使用 MockMvc 实例构建 WebDriver
,我们可以随时使用 MockMvc 的所有功能。
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 |