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,就不必更新所有測試。

我們甚至可以更進一步,將這段邏輯放在一個表示當前 HtmlPageObject 中,如下例所示:

  • 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 模式。雖然我們當然可以用 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 的簡單示例。有關更高階的用法,請參閱 高階 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 測試 的設計。正如我們在 為什麼選擇 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 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://:9990/mail/messages/form");
		return PageFactory.initElements(driver, CreateMessagePage.class);
	}
}
1 CreateMessagePage 擴充套件了 AbstractPage。我們不會詳細介紹 AbstractPage 的細節,但總的來說,它包含了我們所有頁面共有的功能。例如,如果我們的應用程式有一個導航欄、全域性錯誤訊息和其他功能,我們可以將這些邏輯放在一個共享的位置。
2 我們為 HTML 頁面中我們感興趣的每個部分都設定了一個成員變數。這些變數的型別是 WebElement。WebDriver 的 PageFactory 允許我們透過自動解析每個 WebElement 來從 HtmlUnit 版本的 CreateMessagePage 中刪除大量程式碼。 PageFactory#initElements(WebDriver,Class<T>) 方法透過使用欄位名,並根據 HTML 頁面中元素的 idname 來查詢,自動解析每個 WebElement
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://:9990/mail/messages/form")
			return PageFactory.initElements(driver, CreateMessagePage::class.java)
		}
	}
}
1 CreateMessagePage 擴充套件了 AbstractPage。我們不會詳細介紹 AbstractPage 的細節,但總的來說,它包含了我們所有頁面共有的功能。例如,如果我們的應用程式有一個導航欄、全域性錯誤訊息和其他功能,我們可以將這些邏輯放在一個共享的位置。
2 我們為 HTML 頁面中我們感興趣的每個部分都設定了一個成員變數。這些變數的型別是 WebElement。WebDriver 的 PageFactory 允許我們透過自動解析每個 WebElement 來從 HtmlUnit 版本的 CreateMessagePage 中刪除大量程式碼。 PageFactory#initElements(WebDriver,Class<T>) 方法透過使用欄位名,並根據 HTML 頁面中元素的 idname 來查詢,自動解析每個 WebElement
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();
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
© . This site is unofficial and not affiliated with VMware.