对于最新的稳定版本,请使用 Spring Framework 6.2.0! |
MockMvc 和 WebDriver
在前面的部分中,我们已经了解了如何将 MockMvc 与原始 HtmlUnit API 的 API 中。在本节中,我们使用 Selenium WebDriver 中的其他抽象来使事情变得更加容易。
为什么选择 WebDriver 和 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)
}
这样做可以确保在更改 UI 时不必更新所有测试。
我们甚至可以更进一步,将这个逻辑放在Object
那
表示HtmlPage
我们目前处于 on,如下例所示:
-
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 和 WebDriver 设置
要将 Selenium WebDriver 与 Spring MVC 测试框架一起使用,请确保您的项目
包括org.seleniumhq.selenium:selenium-htmlunit-driver
.
我们可以使用MockMvcHtmlUnitDriverBuilder
如下例所示:
-
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
作为服务器
定向到我们的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)
这通过利用 Page Object Pattern 改进了我们的 HtmlUnit 测试的设计。正如我们在 为什么是 WebDriver 和 MockMvc?中提到的,我们可以使用 Page 对象模式
使用 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 ,但总的来说,它包含我们所有页面的通用功能。
例如,如果我们的应用程序有一个导航栏、全局错误消息和其他
功能,我们可以将此 logic 放在共享位置。 |
2 | 我们所在的 HTML 页面的每个部分都有一个成员变量
感兴趣。这些是属于WebElement .WebDriver 的PageFactory 让我们删除一个
来自 HtmlUnit 版本的CreateMessagePage 通过自动解析
每WebElement .这PageFactory#initElements(WebDriver,Class<T>) 方法会自动解析每个WebElement 通过使用字段名称并查找它
由id 或name 的元素。 |
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 ,但总的来说,它包含我们所有页面的通用功能。
例如,如果我们的应用程序有一个导航栏、全局错误消息和其他
功能,我们可以将此 logic 放在共享位置。 |
2 | 我们所在的 HTML 页面的每个部分都有一个成员变量
感兴趣。这些是属于WebElement .WebDriver 的PageFactory 让我们删除一个
来自 HtmlUnit 版本的CreateMessagePage 通过自动解析
每WebElement .这PageFactory#initElements(WebDriver,Class<T>) 方法会自动解析每个WebElement 通过使用字段名称并查找它
由id 或name 的元素。 |
3 | 我们可以使用@FindBy 注解以覆盖默认查找行为。我们的示例展示了如何使用@FindBy 注解来查找我们的提交按钮,并使用css selector (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())
然后,我们可以在断言中使用丰富的域对象。
最后,我们不能忘记关闭WebDriver
实例,
如下:
-
Java
-
Kotlin
@AfterEach
void destroy() {
if (driver != null) {
driver.close();
}
}
@AfterEach
fun destroy() {
if (driver != null) {
driver.close()
}
}
有关使用 WebDriver 的更多信息,请参阅 Selenium WebDriver 文档。
高深MockMvcHtmlUnitDriverBuilder
在到目前为止的示例中,我们使用了MockMvcHtmlUnitDriverBuilder
以最简单的方式
可能,通过构建一个WebDriver
基于WebApplicationContext
为我们加载
Spring TestContext 框架。此处重复此方法,如下所示:
-
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
这更详细,但是,通过构建WebDriver
替换为MockMvc
实例中,我们有
MockMvc 的全部功能触手可及。
有关创建MockMvc 实例,请参阅 设置选项。 |