MockMvc 與 WebDriver
在前幾節中,我們已經了解如何將 MockMvc 與原始 HtmlUnit API 結合使用。在本節中,我們將使用 Selenium WebDriver 中的其他抽象概念,使事情變得更加容易。
為何選擇 WebDriver 與 MockMvc?
我們已經可以使用 HtmlUnit 與 MockMvc,那麼為何還要使用 WebDriver?Selenium WebDriver 提供了一個非常優雅的 API,讓我們可以輕鬆地組織程式碼。為了更好地展示其運作方式,我們將在本節中探索一個範例。
儘管 WebDriver 是 Selenium 的一部分,但它不需要 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,就不必更新所有測試。
我們甚至可以更進一步,將此邏輯放置在代表我們目前所在 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 與 WebDriver 設定
若要將 Selenium WebDriver 與 MockMvc
搭配使用,請確保您的專案包含對 org.seleniumhq.selenium:selenium-htmlunit3-driver
的測試依賴。
我們可以輕鬆地建立與 MockMvc 整合的 Selenium WebDriver,方法是使用 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
作為伺服器的 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)
這改進了我們的 HtmlUnit 測試的設計,方法是利用 Page Object Pattern。正如我們在 為何選擇 WebDriver 與 MockMvc?中提到的,我們可以使用 Page Object Pattern 與 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("https://127.0.0.1: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 註解 來覆寫預設查找行為。我們的範例示範如何使用 @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("https://127.0.0.1:9990/mail/messages/form")
return PageFactory.initElements(driver, CreateMessagePage::class.java)
}
}
}
1 | CreateMessagePage 擴充了 AbstractPage 。我們不會詳細介紹 AbstractPage ,但總之,它包含我們所有頁面的通用功能。例如,如果我們的應用程式有導覽列、全域錯誤訊息和其他功能,我們可以將此邏輯放置在共用位置。 |
2 | 我們為我們感興趣的 HTML 頁面的每個部分都有一個成員變數。這些變數的類型為 WebElement 。WebDriver 的 PageFactory 讓我們可以從 CreateMessagePage 的 HtmlUnit 版本中移除大量程式碼,方法是自動解析每個 WebElement 。PageFactory#initElements(WebDriver,Class<T>) 方法會自動解析每個 WebElement ,方法是使用欄位名稱,並依據 HTML 頁面中元素的 id 或 name 進行查找。 |
3 | 我們可以使用 @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 Framework 為我們載入的 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 的完整功能。
如需有關建立 MockMvc 實例的其他資訊,請參閱組態 MockMvc。 |