Document AssertJ support for MockMvc
This commit restructures the section on MockMvc so that the anchors are easier to read. The standard integration has moved to a Hamcrest Integration section at the same level as HtmlUnit Integration, and a new AssertJ Integration section has been created. Closes gh-32454
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
[[mockmvc-server-htmlunit-geb]]
|
||||
= MockMvc and Geb
|
||||
|
||||
In the previous section, we saw how to use MockMvc with WebDriver. In this section, we
|
||||
use https://www.gebish.org/[Geb] to make our tests even Groovy-er.
|
||||
|
||||
[[mockmvc-server-htmlunit-geb-why]]
|
||||
== Why Geb and MockMvc?
|
||||
|
||||
Geb is backed by WebDriver, so it offers many of the
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from
|
||||
WebDriver. However, Geb makes things even easier by taking care of some of the
|
||||
boilerplate code for us.
|
||||
|
||||
[[mockmvc-server-htmlunit-geb-setup]]
|
||||
== MockMvc and Geb Setup
|
||||
|
||||
We can easily initialize a Geb `Browser` with a Selenium WebDriver that uses MockMvc, as
|
||||
follows:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
def setup() {
|
||||
browser.driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build()
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
|
||||
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
|
||||
|
||||
This ensures that any URL referencing `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-server-htmlunit-geb-usage]]
|
||||
== MockMvc and Geb Usage
|
||||
|
||||
Now we can use Geb 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:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
to CreateMessagePage
|
||||
----
|
||||
|
||||
We can then fill out the form and submit it to create a message, as follows:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
when:
|
||||
form.summary = expectedSummary
|
||||
form.text = expectedMessage
|
||||
submit.click(ViewMessagePage)
|
||||
----
|
||||
|
||||
Any unrecognized method calls or property accesses or references that are not found are
|
||||
forwarded to the current page object. This removes a lot of the boilerplate code we
|
||||
needed when using WebDriver directly.
|
||||
|
||||
As with direct WebDriver usage, this improves on the design of our
|
||||
xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object
|
||||
Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and
|
||||
WebDriver, but it is even easier with Geb. Consider our new Groovy-based
|
||||
`CreateMessagePage` implementation:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
class CreateMessagePage extends Page {
|
||||
static url = 'messages/form'
|
||||
static at = { assert title == 'Messages : Create'; true }
|
||||
static content = {
|
||||
submit { $('input[type=submit]') }
|
||||
form { $('form') }
|
||||
errors(required:false) { $('label.error, .alert-error')?.text() }
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Our `CreateMessagePage` extends `Page`. We do not go over the details of `Page`, but, in
|
||||
summary, it contains common functionality for all of our pages. We define a URL in which
|
||||
this page can be found. This lets us navigate to the page, as follows:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
to CreateMessagePage
|
||||
----
|
||||
|
||||
We also have an `at` closure that determines if we are at the specified page. It should
|
||||
return `true` if we are on the correct page. This is why we can assert that we are on the
|
||||
correct page, as follows:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
then:
|
||||
at CreateMessagePage
|
||||
errors.contains('This field is required.')
|
||||
----
|
||||
|
||||
NOTE: We use an assertion in the closure so that we can determine where things went wrong
|
||||
if we were at the wrong page.
|
||||
|
||||
Next, we create a `content` closure that specifies all the areas of interest within the
|
||||
page. We can use a
|
||||
https://www.gebish.org/manual/current/#the-jquery-ish-navigator-api[jQuery-ish Navigator
|
||||
API] to select the content in which we are interested.
|
||||
|
||||
Finally, we can verify that a new message was created successfully, as follows:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
then:
|
||||
at ViewMessagePage
|
||||
success == 'Successfully created a new message'
|
||||
id
|
||||
date
|
||||
summary == expectedSummary
|
||||
message == expectedMessage
|
||||
----
|
||||
|
||||
For further details on how to get the most out of Geb, see
|
||||
https://www.gebish.org/manual/current/[The Book of Geb] user's manual.
|
||||
@@ -0,0 +1,279 @@
|
||||
[[mockmvc-server-htmlunit-mah]]
|
||||
= MockMvc and HtmlUnit
|
||||
|
||||
This section describes how to integrate MockMvc and HtmlUnit. Use this option if you want
|
||||
to use the raw HtmlUnit libraries.
|
||||
|
||||
[[mockmvc-server-htmlunit-mah-setup]]
|
||||
== MockMvc and HtmlUnit Setup
|
||||
|
||||
First, make sure that you have included a test dependency on
|
||||
`org.htmlunit:htmlunit`.
|
||||
|
||||
We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the
|
||||
`MockMvcWebClientBuilder`, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
WebClient webClient;
|
||||
|
||||
@BeforeEach
|
||||
void setup(WebApplicationContext context) {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
lateinit var webClient: WebClient
|
||||
|
||||
@BeforeEach
|
||||
fun setup(context: WebApplicationContext) {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage,
|
||||
see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
|
||||
|
||||
This 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-server-htmlunit-mah-usage]]
|
||||
== MockMvc and HtmlUnit Usage
|
||||
|
||||
Now we can use HtmlUnit 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:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The default context path is `""`. Alternatively, we can specify the context path,
|
||||
as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
|
||||
|
||||
Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it
|
||||
to create a message, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
|
||||
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute("Spring Rocks");
|
||||
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
|
||||
textInput.setText("In case you didn't know, Spring Rocks!");
|
||||
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
|
||||
HtmlPage newMessagePage = submit.click();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val form = createMsgFormPage.getHtmlElementById("messageForm")
|
||||
val summaryInput = createMsgFormPage.getHtmlElementById("summary")
|
||||
summaryInput.setValueAttribute("Spring Rocks")
|
||||
val textInput = createMsgFormPage.getHtmlElementById("text")
|
||||
textInput.setText("In case you didn't know, Spring Rocks!")
|
||||
val submit = form.getOneHtmlElementByAttribute("input", "type", "submit")
|
||||
val newMessagePage = submit.click()
|
||||
----
|
||||
======
|
||||
|
||||
Finally, we can verify that a new message was created successfully. The following
|
||||
assertions use the {assertj-docs}[AssertJ] library:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
|
||||
String id = newMessagePage.getHtmlElementById("id").getTextContent();
|
||||
assertThat(id).isEqualTo("123");
|
||||
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
|
||||
assertThat(summary).isEqualTo("Spring Rocks");
|
||||
String text = newMessagePage.getHtmlElementById("text").getTextContent();
|
||||
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
|
||||
val id = newMessagePage.getHtmlElementById("id").getTextContent()
|
||||
assertThat(id).isEqualTo("123")
|
||||
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
|
||||
assertThat(summary).isEqualTo("Spring Rocks")
|
||||
val text = newMessagePage.getHtmlElementById("text").getTextContent()
|
||||
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")
|
||||
----
|
||||
======
|
||||
|
||||
The preceding code improves on our
|
||||
xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways.
|
||||
First, we no longer have to explicitly verify our form and then create a request that
|
||||
looks like the form. Instead, we request the form, fill it out, and submit it, thereby
|
||||
significantly reducing the overhead.
|
||||
|
||||
Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit
|
||||
uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test
|
||||
the behavior of JavaScript within our pages.
|
||||
|
||||
See the https://htmlunit.sourceforge.io/gettingStarted.html[HtmlUnit documentation] for
|
||||
additional information about using HtmlUnit.
|
||||
|
||||
[[mockmvc-server-htmlunit-mah-advanced-builder]]
|
||||
== Advanced `MockMvcWebClientBuilder`
|
||||
|
||||
In the examples so far, we have used `MockMvcWebClientBuilder` in the simplest way
|
||||
possible, by building a `WebClient` based on the `WebApplicationContext` loaded for us by
|
||||
the Spring TestContext Framework. This approach is repeated in the following example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
WebClient webClient;
|
||||
|
||||
@BeforeEach
|
||||
void setup(WebApplicationContext context) {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
lateinit var webClient: WebClient
|
||||
|
||||
@BeforeEach
|
||||
fun setup(context: WebApplicationContext) {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
We can also specify additional configuration options, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
WebClient webClient;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
// 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();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
lateinit var webClient: WebClient
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
// 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()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
As an alternative, we can perform the exact same setup by configuring the `MockMvc`
|
||||
instance separately and supplying it to the `MockMvcWebClientBuilder`, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
MockMvc mockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply(springSecurity())
|
||||
.build();
|
||||
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.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();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
----
|
||||
======
|
||||
|
||||
This is more verbose, but, by building the `WebClient` with a `MockMvc` instance, we have
|
||||
the full power of MockMvc at our fingertips.
|
||||
|
||||
TIP: For additional information on creating a `MockMvc` instance, see
|
||||
xref:testing/mockmvc/hamcrest/setup.adoc[Configuring MockMvc].
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
[[mockmvc-server-htmlunit-webdriver]]
|
||||
= MockMvc and 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
|
||||
https://docs.seleniumhq.org/projects/webdriver/[WebDriver] to make things even easier.
|
||||
|
||||
[[mockmvc-server-htmlunit-webdriver-why]]
|
||||
== Why WebDriver and MockMvc?
|
||||
|
||||
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.
|
||||
|
||||
NOTE: Despite being a part of https://docs.seleniumhq.org/[Selenium], WebDriver does not
|
||||
require a Selenium Server to run your tests.
|
||||
|
||||
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.
|
||||
|
||||
If one of the fields were named "`summary`", we might have something that resembles the
|
||||
following repeated in multiple places within our tests:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute(summary);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val summaryInput = currentPage.getHtmlElementById("summary")
|
||||
summaryInput.setValueAttribute(summary)
|
||||
----
|
||||
======
|
||||
|
||||
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:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
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);
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
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());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Formerly, this pattern was known as the
|
||||
https://github.com/SeleniumHQ/selenium/wiki/PageObjects[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-server-htmlunit-webdriver-setup]]
|
||||
== MockMvc and WebDriver Setup
|
||||
|
||||
To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test
|
||||
dependency on `org.seleniumhq.selenium:selenium-htmlunit3-driver`.
|
||||
|
||||
We can easily create a Selenium WebDriver that integrates with MockMvc by using the
|
||||
`MockMvcHtmlUnitDriverBuilder` as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
WebDriver driver;
|
||||
|
||||
@BeforeEach
|
||||
void setup(WebApplicationContext context) {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
lateinit var driver: WebDriver
|
||||
|
||||
@BeforeEach
|
||||
fun setup(context: WebApplicationContext) {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
|
||||
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
|
||||
|
||||
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-server-htmlunit-webdriver-usage]]
|
||||
== MockMvc and WebDriver Usage
|
||||
|
||||
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:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
CreateMessagePage page = CreateMessagePage.to(driver);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val page = CreateMessagePage.to(driver)
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
We can then fill out the form and submit it to create a message, as follows:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
ViewMessagePage viewMessagePage =
|
||||
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val viewMessagePage =
|
||||
page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
|
||||
by leveraging the Page Object Pattern. As we mentioned in
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[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:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
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
|
||||
https://github.com/SeleniumHQ/selenium/wiki/PageFactory[`PageFactory`] lets us remove a
|
||||
lot of code from the HtmlUnit version of `CreateMessagePage` by automatically resolving
|
||||
each `WebElement`. The
|
||||
https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`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
|
||||
https://github.com/SeleniumHQ/selenium/wiki/PageFactory#making-the-example-work-using-annotations[`@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]`).
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
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` 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
|
||||
https://github.com/SeleniumHQ/selenium/wiki/PageFactory[`PageFactory`] lets us remove a
|
||||
lot of code from the HtmlUnit version of `CreateMessagePage` by automatically resolving
|
||||
each `WebElement`. The
|
||||
https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-[`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
|
||||
https://github.com/SeleniumHQ/selenium/wiki/PageFactory#making-the-example-work-using-annotations[`@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]*).
|
||||
======
|
||||
--
|
||||
|
||||
Finally, we can verify that a new message was created successfully. The following
|
||||
assertions use the {assertj-docs}[AssertJ] assertion library:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
|
||||
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
|
||||
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new 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:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
public Message getMessage() throws ParseException {
|
||||
Message message = new Message();
|
||||
message.setId(getId());
|
||||
message.setCreated(getCreated());
|
||||
message.setSummary(getSummary());
|
||||
message.setText(getText());
|
||||
return message;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
fun getMessage() = Message(getId(), getCreated(), getSummary(), getText())
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
We can then use the rich domain objects in our assertions.
|
||||
|
||||
Lastly, we must not forget to close the `WebDriver` instance when the test is complete,
|
||||
as follows:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@AfterEach
|
||||
void destroy() {
|
||||
if (driver != null) {
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@AfterEach
|
||||
fun destroy() {
|
||||
if (driver != null) {
|
||||
driver.close()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
For additional information on using WebDriver, see the Selenium
|
||||
https://github.com/SeleniumHQ/selenium/wiki/Getting-Started[WebDriver documentation].
|
||||
|
||||
[[mockmvc-server-htmlunit-webdriver-advanced-builder]]
|
||||
== Advanced `MockMvcHtmlUnitDriverBuilder`
|
||||
|
||||
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:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
WebDriver driver;
|
||||
|
||||
@BeforeEach
|
||||
void setup(WebApplicationContext context) {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
lateinit var driver: WebDriver
|
||||
|
||||
@BeforeEach
|
||||
fun setup(context: WebApplicationContext) {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
We can also specify additional configuration options, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
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();
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
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()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
As an alternative, we can perform the exact same setup by configuring the `MockMvc`
|
||||
instance separately and supplying it to the `MockMvcHtmlUnitDriverBuilder`, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
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();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
----
|
||||
======
|
||||
|
||||
This is more verbose, but, by building the `WebDriver` with a `MockMvc` instance, we have
|
||||
the full power of MockMvc at our fingertips.
|
||||
|
||||
TIP: For additional information on creating a `MockMvc` instance, see
|
||||
xref:testing/mockmvc/hamcrest/setup.adoc[Configuring MockMvc].
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
[[mockmvc-server-htmlunit-why]]
|
||||
= Why HtmlUnit Integration?
|
||||
|
||||
The most obvious question that comes to mind is "`Why do I need this?`" The answer is
|
||||
best found by exploring a very basic sample application. Assume you have a Spring MVC web
|
||||
application that supports CRUD operations on a `Message` object. The application also
|
||||
supports paging through all messages. How would you go about testing it?
|
||||
|
||||
With Spring MVC Test, we can easily test if we are able to create a `Message`, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
MockHttpServletRequestBuilder createMessage = post("/messages/")
|
||||
.param("summary", "Spring Rocks")
|
||||
.param("text", "In case you didn't know, Spring Rocks!");
|
||||
|
||||
mockMvc.perform(createMessage)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/messages/123"));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@Test
|
||||
fun test() {
|
||||
mockMvc.post("/messages/") {
|
||||
param("summary", "Spring Rocks")
|
||||
param("text", "In case you didn't know, Spring Rocks!")
|
||||
}.andExpect {
|
||||
status().is3xxRedirection()
|
||||
redirectedUrl("/messages/123")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
What if we want to test the form view that lets us create the message? For example,
|
||||
assume our form looks like the following snippet:
|
||||
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
<form id="messageForm" action="/messages/" method="post">
|
||||
<div class="pull-right"><a href="/messages/">Messages</a></div>
|
||||
|
||||
<label for="summary">Summary</label>
|
||||
<input type="text" class="required" id="summary" name="summary" value="" />
|
||||
|
||||
<label for="text">Message</label>
|
||||
<textarea id="text" name="text"></textarea>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="Create" />
|
||||
</div>
|
||||
</form>
|
||||
----
|
||||
|
||||
How do we ensure that our form produce the correct request to create a new message? A
|
||||
naive attempt might resemble the following:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
mockMvc.perform(get("/messages/form"))
|
||||
.andExpect(xpath("//input[@name='summary']").exists())
|
||||
.andExpect(xpath("//textarea[@name='text']").exists());
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
mockMvc.get("/messages/form").andExpect {
|
||||
xpath("//input[@name='summary']") { exists() }
|
||||
xpath("//textarea[@name='text']") { exists() }
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
This test has some obvious drawbacks. If we update our controller to use the parameter
|
||||
`message` instead of `text`, our form test continues to pass, even though the HTML form
|
||||
is out of sync with the controller. To resolve this we can combine our two tests, as
|
||||
follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[[mockmvc-server-htmlunit-mock-mvc-test]]
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
String summaryParamName = "summary";
|
||||
String textParamName = "text";
|
||||
mockMvc.perform(get("/messages/form"))
|
||||
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
|
||||
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
|
||||
|
||||
MockHttpServletRequestBuilder createMessage = post("/messages/")
|
||||
.param(summaryParamName, "Spring Rocks")
|
||||
.param(textParamName, "In case you didn't know, Spring Rocks!");
|
||||
|
||||
mockMvc.perform(createMessage)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/messages/123"));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val summaryParamName = "summary";
|
||||
val textParamName = "text";
|
||||
mockMvc.get("/messages/form").andExpect {
|
||||
xpath("//input[@name='$summaryParamName']") { exists() }
|
||||
xpath("//textarea[@name='$textParamName']") { exists() }
|
||||
}
|
||||
mockMvc.post("/messages/") {
|
||||
param(summaryParamName, "Spring Rocks")
|
||||
param(textParamName, "In case you didn't know, Spring Rocks!")
|
||||
}.andExpect {
|
||||
status().is3xxRedirection()
|
||||
redirectedUrl("/messages/123")
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
This would reduce the risk of our test incorrectly passing, but there are still some
|
||||
problems:
|
||||
|
||||
* What if we have multiple forms on our page? Admittedly, we could update our XPath
|
||||
expressions, but they get more complicated as we take more factors into account: Are
|
||||
the fields the correct type? Are the fields enabled? And so on.
|
||||
* Another issue is that we are doing double the work we would expect. We must first
|
||||
verify the view, and then we submit the view with the same parameters we just verified.
|
||||
Ideally, this could be done all at once.
|
||||
* Finally, we still cannot account for some things. For example, what if the form has
|
||||
JavaScript validation that we wish to test as well?
|
||||
|
||||
The overall problem is that testing a web page does not involve a single interaction.
|
||||
Instead, it is a combination of how the user interacts with a web page and how that web
|
||||
page interacts with other resources. For example, the result of a form view is used as
|
||||
the input to a user for creating a message. In addition, our form view can potentially
|
||||
use additional resources that impact the behavior of the page, such as JavaScript
|
||||
validation.
|
||||
|
||||
[[mockmvc-server-htmlunit-why-integration]]
|
||||
== Integration Testing to the Rescue?
|
||||
|
||||
To resolve the issues mentioned earlier, we could perform end-to-end integration testing,
|
||||
but this has some drawbacks. Consider testing the view that lets us page through the
|
||||
messages. We might need the following tests:
|
||||
|
||||
* Does our page display a notification to the user to indicate that no results are
|
||||
available when the messages are empty?
|
||||
* Does our page properly display a single message?
|
||||
* Does our page properly support paging?
|
||||
|
||||
To set up these tests, we need to ensure our database contains the proper messages. This
|
||||
leads to a number of additional challenges:
|
||||
|
||||
* Ensuring the proper messages are in the database can be tedious. (Consider foreign key
|
||||
constraints.)
|
||||
* Testing can become slow, since each test would need to ensure that the database is in
|
||||
the correct state.
|
||||
* Since our database needs to be in a specific state, we cannot run tests in parallel.
|
||||
* Performing assertions on such items as auto-generated IDs, timestamps, and others can
|
||||
be difficult.
|
||||
|
||||
These challenges do not mean that we should abandon end-to-end integration testing
|
||||
altogether. Instead, we can reduce the number of end-to-end integration tests by
|
||||
refactoring our detailed tests to use mock services that run much faster, more reliably,
|
||||
and without side effects. We can then implement a small number of true end-to-end
|
||||
integration tests that validate simple workflows to ensure that everything works together
|
||||
properly.
|
||||
|
||||
[[mockmvc-server-htmlunit-why-mockmvc]]
|
||||
== Enter HtmlUnit Integration
|
||||
|
||||
So how can we achieve a balance between testing the interactions of our pages and still
|
||||
retain good performance within our test suite? The answer is: "`By integrating MockMvc
|
||||
with HtmlUnit.`"
|
||||
|
||||
[[mockmvc-server-htmlunit-options]]
|
||||
== HtmlUnit Integration Options
|
||||
|
||||
You have a number of options when you want to integrate MockMvc with HtmlUnit:
|
||||
|
||||
* xref:testing/mockmvc/htmlunit/mah.adoc[MockMvc and HtmlUnit]: Use this option if you
|
||||
want to use the raw HtmlUnit libraries.
|
||||
* xref:testing/mockmvc/htmlunit/webdriver.adoc[MockMvc and WebDriver]: Use this option to
|
||||
ease development and reuse code between integration and end-to-end testing.
|
||||
* xref:testing/mockmvc/htmlunit/geb.adoc[MockMvc and Geb]: Use this option if you want to
|
||||
use Groovy for testing, ease development, and reuse code between integration and
|
||||
end-to-end testing.
|
||||
|
||||
Reference in New Issue
Block a user