MockMvc和WebDriver
在前面的章节中,我们已经了解了如何将MockMvc与原始的HtmlUnit API结合使用。在本节中,我们将利用Selenium WebDriver中的额外抽象层,使操作更加简便。
为什么选择WebDriver和MockMvc?
我们已经可以使用HtmlUnit和MockMvc了,那么为什么还要使用WebDriver呢?Selenium WebDriver提供了一个非常优雅的API,让我们能够轻松地组织代码。为了更好地展示其工作原理,本节我们将通过一个示例来进行探讨。
尽管WebDriver是Selenium的一部分,但运行测试时并不需要Selenium服务器。
假设我们需要确保消息能够正确创建。测试包括找到HTML表单的输入元素,填写这些元素,并进行各种断言(assertions)。
这种方法导致了大量的单独测试,因为我们还想测试错误情况。例如,我们想要确保如果只填写了表单的一部分,也会出现错误。如果我们填写了整个表单,那么之后应该会显示新创建的消息。
如果其中一个字段的名称是“summary”,那么在我们的测试中,可能会在多个地方重复出现类似以下的内容:
- Java
- Kotlin
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
那么,如果我们把id改为smmry会怎么样呢?这样做将迫使我们更新所有的测试以包含这一变化。这违反了DRY原则(Don’t Repeat Yourself,不要重复自己),因此我们理想上应该将这些代码提取到一个单独的方法中,如下所示:
- 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),就不必更新所有的测试用例了。
我们甚至可以更进一步,将这种逻辑放在一个表示我们当前所在“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()
}
}
}
以前,这种模式被称为页面对象模式。虽然我们当然也可以使用HtmlUnit来实现这一点,但WebDriver提供了一些工具,在后续章节中我们会探讨这些工具,让这种模式的实现变得更加容易。
MockMvc和WebDriver设置
要使用Selenium WebDriver与MockMvc一起工作,请确保您的项目包含了org.seleniumhq.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()
}
这是一个使用MockMvcHtmlUnitDriverBuilder的简单示例。有关更高级的用法,请参阅Advanced MockMvcHtmlUnitDriverBuilder。
前面的例子确保了任何将 localhost 作为服务器的 URL 都会被导向到我们的 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?中提到的,我们也可以在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 CreateMessagePageWebDriver driver) {
super(driver);
}
public <T> T createMessage(Class<T> resultPage, String summary, String details) {
this.summary.sendKeys(summary);
this.textsendKeys(details);
this.submit.click();
return PageFactory.initElements(driver, resultPage);
}
public static CreateMessagePage toWebDriver driver) {
.driver.get("http://localhost:9990/mail/messages/form");
return PageFactory.initElements(driver, CreateMessagePage.class);
}
}
CreateMessagePage继承自AbstractPage。我们不详细讨论AbstractPage的内容,但简而言之,它包含了我们所有页面共用的功能。例如,如果我们的应用程序有导航栏、全局错误信息等其他功能,我们可以将这些逻辑放在一个共享的位置。我们为 HTML 页面中感兴趣的每个部分都定义了成员变量,这些变量的类型是
WebElement。WebDriver 的 PageFactory 通过自动解析每个WebElement,让我们能够减少CreateMessagePage(基于 HtmlUnit 的版本)中的大量代码。PageFactory#initElements(driver,Class<T>)方法会根据 HTML 页面中元素的id或name来自动解析每个WebElement。我们可以使用
@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)
}
}
}
CreateMessagePage继承自AbstractPage。我们不详细讨论AbstractPage的内容,但简而言之,它包含了我们所有页面共用的功能。例如,如果我们的应用程序有导航栏、全局错误信息等其他功能,我们可以将这些逻辑放在一个共享的位置。我们为 HTML 页面中感兴趣的每个部分都定义了成员变量,这些变量的类型是
WebElement。WebDriver 的 PageFactory 通过自动解析每个WebElement,让我们能够减少CreateMessagePage(基于 HtmlUnit 的版本)中的大量代码。PageFactory#initElements(driver,Class<T>)方法会根据 HTML 页面中元素的id或name来自动解析每个WebElement。我们可以使用
@FindBy注解来覆盖默认的查找行为。在我们的示例中,展示了如何使用@FindBy注解,并通过css选择器 (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,即基于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();
val mockMvc: MockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply<DefaultMockMvcBuilder>(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()
这可能显得有些冗长,但是,通过使用MockMvc实例来构建WebDriver,我们就可以充分利用MockMvc的所有功能了。
有关创建MockMvc实例的更多信息,请参阅配置MockMvc。