1875 lines
64 KiB
Plaintext
1875 lines
64 KiB
Plaintext
[[spring-mvc-test-framework]]
|
|
= MockMvc
|
|
|
|
The Spring MVC Test framework, also known as MockMvc, provides support for testing Spring
|
|
MVC applications. It performs full Spring MVC request handling but via mock request and
|
|
response objects instead of a running server.
|
|
|
|
MockMvc can be used on its own to perform requests and verify responses. It can also be
|
|
used through the <<webtestclient>> where MockMvc is plugged in as the server to handle
|
|
requests with. The advantage of `WebTestClient` is the option to work with higher level
|
|
objects instead of raw data as well as the ability to switch to full, end-to-end HTTP
|
|
tests against a live server and use the same test API.
|
|
|
|
|
|
[[spring-mvc-test-server]]
|
|
== Overview
|
|
|
|
You can write plain unit tests for Spring MVC by instantiating a controller, injecting it
|
|
with dependencies, and calling its methods. However such tests do not verify request
|
|
mappings, data binding, message conversion, type conversion, validation, and nor
|
|
do they involve any of the supporting `@InitBinder`, `@ModelAttribute`, or
|
|
`@ExceptionHandler` methods.
|
|
|
|
The Spring MVC Test framework, also known as `MockMvc`, aims to provide more complete
|
|
testing for Spring MVC controllers without a running server. It does that by invoking
|
|
the `DispatcherServlet` and passing
|
|
<<mock-objects-servlet, "`mock`" implementations of the Servlet API>> from the
|
|
`spring-test` module which replicates the full Spring MVC request handling without
|
|
a running server.
|
|
|
|
MockMvc is a server side test framework that lets you verify most of the functionality
|
|
of a Spring MVC application using lightweight and targeted tests. You can use it on
|
|
its own to perform requests and to verify responses, or you can also use it through
|
|
the <<webtestclient>> API with MockMvc plugged in as the server to handle requests
|
|
with.
|
|
|
|
|
|
[[spring-mvc-test-server-static-imports]]
|
|
== Static Imports
|
|
|
|
When using MockMvc directly to perform requests, you'll need static imports for:
|
|
|
|
- `MockMvcBuilders.{asterisk}`
|
|
- `MockMvcRequestBuilders.{asterisk}`
|
|
- `MockMvcResultMatchers.{asterisk}`
|
|
- `MockMvcResultHandlers.{asterisk}`
|
|
|
|
An easy way to remember that is search for `MockMvc*`. If using Eclipse be sure to also
|
|
add the above as "`favorite static members`" in the Eclipse preferences.
|
|
|
|
When using MockMvc through the <<webtestclient>> you do not need static imports.
|
|
The `WebTestClient` provides a fluent API without static imports.
|
|
|
|
|
|
[[spring-mvc-test-server-setup-options]]
|
|
== Setup Choices
|
|
|
|
MockMvc can be setup in one of two ways. One is to point directly to the controllers you
|
|
want to test and programmatically configure Spring MVC infrastructure. The second is to
|
|
point to Spring configuration with Spring MVC and controller infrastructure in it.
|
|
|
|
To set up MockMvc for testing a specific controller, use the following:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
class MyWebTests {
|
|
|
|
MockMvc mockMvc;
|
|
|
|
@BeforeEach
|
|
void setup() {
|
|
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
class MyWebTests {
|
|
|
|
lateinit var mockMvc : MockMvc
|
|
|
|
@BeforeEach
|
|
fun setup() {
|
|
mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build()
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
Or you can also use this setup when testing through the
|
|
<<webtestclient-controller-config, WebTestClient>> which delegates to the same builder
|
|
as shown above.
|
|
|
|
To set up MockMvc through Spring configuration, use the following:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
@SpringJUnitWebConfig(locations = "my-servlet-context.xml")
|
|
class MyWebTests {
|
|
|
|
MockMvc mockMvc;
|
|
|
|
@BeforeEach
|
|
void setup(WebApplicationContext wac) {
|
|
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
@SpringJUnitWebConfig(locations = ["my-servlet-context.xml"])
|
|
class MyWebTests {
|
|
|
|
lateinit var mockMvc: MockMvc
|
|
|
|
@BeforeEach
|
|
fun setup(wac: WebApplicationContext) {
|
|
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
Or you can also use this setup when testing through the
|
|
<<webtestclient-context-config, WebTestClient>> which delegates to the same builder
|
|
as shown above.
|
|
|
|
|
|
|
|
Which setup option should you use?
|
|
|
|
The `webAppContextSetup` loads your actual Spring MVC configuration, resulting in a more
|
|
complete integration test. Since the TestContext framework caches the loaded Spring
|
|
configuration, it helps keep tests running fast, even as you introduce more tests in your
|
|
test suite. Furthermore, you can inject mock services into controllers through Spring
|
|
configuration to remain focused on testing the web layer. The following example declares
|
|
a mock service with Mockito:
|
|
|
|
[source,xml,indent=0,subs="verbatim,quotes"]
|
|
----
|
|
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
|
|
<constructor-arg value="org.example.AccountService"/>
|
|
</bean>
|
|
----
|
|
|
|
You can then inject the mock service into the test to set up and verify your
|
|
expectations, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
|
|
class AccountTests {
|
|
|
|
@Autowired
|
|
AccountService accountService;
|
|
|
|
MockMvc mockMvc;
|
|
|
|
@BeforeEach
|
|
void setup(WebApplicationContext wac) {
|
|
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
@SpringJUnitWebConfig(locations = ["test-servlet-context.xml"])
|
|
class AccountTests {
|
|
|
|
@Autowired
|
|
lateinit var accountService: AccountService
|
|
|
|
lateinit mockMvc: MockMvc
|
|
|
|
@BeforeEach
|
|
fun setup(wac: WebApplicationContext) {
|
|
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build()
|
|
}
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
The `standaloneSetup`, on the other hand, is a little closer to a unit test. It tests one
|
|
controller at a time. You can manually inject the controller with mock dependencies, and
|
|
it does not involve loading Spring configuration. Such tests are more focused on style
|
|
and make it easier to see which controller is being tested, whether any specific Spring
|
|
MVC configuration is required to work, and so on. The `standaloneSetup` is also a very
|
|
convenient way to write ad-hoc tests to verify specific behavior or to debug an issue.
|
|
|
|
As with most "`integration versus unit testing`" debates, there is no right or wrong
|
|
answer. However, using the `standaloneSetup` does imply the need for additional
|
|
`webAppContextSetup` tests in order to verify your Spring MVC configuration.
|
|
Alternatively, you can write all your tests with `webAppContextSetup`, in order to always
|
|
test against your actual Spring MVC configuration.
|
|
|
|
[[spring-mvc-test-server-setup-steps]]
|
|
== Setup Features
|
|
|
|
No matter which MockMvc builder you use, all `MockMvcBuilder` implementations provide
|
|
some common and very useful features. For example, you can declare an `Accept` header for
|
|
all requests and expect a status of 200 as well as a `Content-Type` header in all
|
|
responses, as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
// static import of MockMvcBuilders.standaloneSetup
|
|
|
|
MockMvc mockMvc = standaloneSetup(new MusicController())
|
|
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
|
|
.alwaysExpect(status().isOk())
|
|
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
|
|
.build();
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
|
|
----
|
|
|
|
In addition, third-party frameworks (and applications) can pre-package setup
|
|
instructions, such as those in a `MockMvcConfigurer`. The Spring Framework has one such
|
|
built-in implementation that helps to save and re-use the HTTP session across requests.
|
|
You can use it as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
// static import of SharedHttpSessionConfigurer.sharedHttpSession
|
|
|
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
|
|
.apply(sharedHttpSession())
|
|
.build();
|
|
|
|
// Use mockMvc to perform requests...
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
|
|
----
|
|
|
|
See the javadoc for
|
|
{api-spring-framework}/test/web/servlet/setup/ConfigurableMockMvcBuilder.html[`ConfigurableMockMvcBuilder`]
|
|
for a list of all MockMvc builder features or use the IDE to explore the available options.
|
|
|
|
[[spring-mvc-test-server-performing-requests]]
|
|
== Performing Requests
|
|
|
|
This section shows how to use MockMvc on its own to perform requests and verify responses.
|
|
If using MockMvc through the `WebTestClient` please see the corresponding section on
|
|
<<webtestclient-tests>> instead.
|
|
|
|
To perform requests that use any HTTP method, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
// static import of MockMvcRequestBuilders.*
|
|
|
|
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.post
|
|
|
|
mockMvc.post("/hotels/{id}", 42) {
|
|
accept = MediaType.APPLICATION_JSON
|
|
}
|
|
----
|
|
|
|
You can also perform file upload requests that internally use
|
|
`MockMultipartHttpServletRequest` so that there is no actual parsing of a multipart
|
|
request. Rather, you have to set it up to be similar to the following example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.multipart
|
|
|
|
mockMvc.multipart("/doc") {
|
|
file("a1", "ABC".toByteArray(charset("UTF8")))
|
|
}
|
|
----
|
|
|
|
You can specify query parameters in URI template style, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
mockMvc.get("/hotels?thing={thing}", "somewhere")
|
|
----
|
|
|
|
You can also add Servlet request parameters that represent either query or form
|
|
parameters, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(get("/hotels").param("thing", "somewhere"));
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.get
|
|
|
|
mockMvc.get("/hotels") {
|
|
param("thing", "somewhere")
|
|
}
|
|
----
|
|
|
|
If application code relies on Servlet request parameters and does not check the query
|
|
string explicitly (as is most often the case), it does not matter which option you use.
|
|
Keep in mind, however, that query parameters provided with the URI template are decoded
|
|
while request parameters provided through the `param(...)` method are expected to already
|
|
be decoded.
|
|
|
|
In most cases, it is preferable to leave the context path and the Servlet path out of the
|
|
request URI. If you must test with the full request URI, be sure to set the `contextPath`
|
|
and `servletPath` accordingly so that request mappings work, as the following example
|
|
shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.get
|
|
|
|
mockMvc.get("/app/main/hotels/{id}") {
|
|
contextPath = "/app"
|
|
servletPath = "/main"
|
|
}
|
|
----
|
|
|
|
In the preceding example, it would be cumbersome to set the `contextPath` and
|
|
`servletPath` with every performed request. Instead, you can set up default request
|
|
properties, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
class MyWebTests {
|
|
|
|
MockMvc mockMvc;
|
|
|
|
@BeforeEach
|
|
void setup() {
|
|
mockMvc = standaloneSetup(new AccountController())
|
|
.defaultRequest(get("/")
|
|
.contextPath("/app").servletPath("/main")
|
|
.accept(MediaType.APPLICATION_JSON)).build();
|
|
}
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
|
|
----
|
|
|
|
The preceding properties affect every request performed through the `MockMvc` instance.
|
|
If the same property is also specified on a given request, it overrides the default
|
|
value. That is why the HTTP method and URI in the default request do not matter, since
|
|
they must be specified on every request.
|
|
|
|
[[spring-mvc-test-server-defining-expectations]]
|
|
== Defining Expectations
|
|
|
|
You can define expectations by appending one or more `andExpect(..)` calls after
|
|
performing a request, as the following example shows. As soon as one expectation fails,
|
|
no other expectations will be asserted.
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
|
|
|
|
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.get
|
|
|
|
mockMvc.get("/accounts/1").andExpect {
|
|
status { isOk() }
|
|
}
|
|
----
|
|
|
|
You can define multiple expectations by appending `andExpectAll(..)` after performing a
|
|
request, as the following example shows. In contrast to `andExpect(..)`,
|
|
`andExpectAll(..)` guarantees that all supplied expectations will be asserted and that
|
|
all failures will be tracked and reported.
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
|
|
|
|
mockMvc.perform(get("/accounts/1")).andExpectAll(
|
|
status().isOk(),
|
|
content().contentType("application/json;charset=UTF-8"));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.get
|
|
|
|
mockMvc.get("/accounts/1").andExpectAll {
|
|
status { isOk() }
|
|
content { contentType(APPLICATION_JSON) }
|
|
}
|
|
----
|
|
|
|
`MockMvcResultMatchers.*` provides a number of expectations, some of which are further
|
|
nested with more detailed expectations.
|
|
|
|
Expectations fall in two general categories. The first category of assertions verifies
|
|
properties of the response (for example, the response status, headers, and content).
|
|
These are the most important results to assert.
|
|
|
|
The second category of assertions goes beyond the response. These assertions let you
|
|
inspect Spring MVC specific aspects, such as which controller method processed the
|
|
request, whether an exception was raised and handled, what the content of the model is,
|
|
what view was selected, what flash attributes were added, and so on. They also let you
|
|
inspect Servlet specific aspects, such as request and session attributes.
|
|
|
|
The following test asserts that binding or validation failed:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(post("/persons"))
|
|
.andExpect(status().isOk())
|
|
.andExpect(model().attributeHasErrors("person"));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.post
|
|
|
|
mockMvc.post("/persons").andExpect {
|
|
status { isOk() }
|
|
model {
|
|
attributeHasErrors("person")
|
|
}
|
|
}
|
|
----
|
|
|
|
Many times, when writing tests, it is useful to dump the results of the performed
|
|
request. You can do so as follows, where `print()` is a static import from
|
|
`MockMvcResultHandlers`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(post("/persons"))
|
|
.andDo(print())
|
|
.andExpect(status().isOk())
|
|
.andExpect(model().attributeHasErrors("person"));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
import org.springframework.test.web.servlet.post
|
|
|
|
mockMvc.post("/persons").andDo {
|
|
print()
|
|
}.andExpect {
|
|
status { isOk() }
|
|
model {
|
|
attributeHasErrors("person")
|
|
}
|
|
}
|
|
----
|
|
|
|
As long as request processing does not cause an unhandled exception, the `print()` method
|
|
prints all the available result data to `System.out`. There is also a `log()` method and
|
|
two additional variants of the `print()` method, one that accepts an `OutputStream` and
|
|
one that accepts a `Writer`. For example, invoking `print(System.err)` prints the result
|
|
data to `System.err`, while invoking `print(myWriter)` prints the result data to a custom
|
|
writer. If you want to have the result data logged instead of printed, you can invoke the
|
|
`log()` method, which logs the result data as a single `DEBUG` message under the
|
|
`org.springframework.test.web.servlet.result` logging category.
|
|
|
|
In some cases, you may want to get direct access to the result and verify something that
|
|
cannot be verified otherwise. This can be achieved by appending `.andReturn()` after all
|
|
other expectations, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
|
|
// ...
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
var mvcResult = mockMvc.post("/persons").andExpect { status { isOk() } }.andReturn()
|
|
// ...
|
|
----
|
|
|
|
If all tests repeat the same expectations, you can set up common expectations once when
|
|
building the `MockMvc` instance, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
standaloneSetup(new SimpleController())
|
|
.alwaysExpect(status().isOk())
|
|
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
|
|
.build()
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
|
|
----
|
|
|
|
Note that common expectations are always applied and cannot be overridden without
|
|
creating a separate `MockMvc` instance.
|
|
|
|
When a JSON response content contains hypermedia links created with
|
|
https://github.com/spring-projects/spring-hateoas[Spring HATEOAS], you can verify the
|
|
resulting links by using JsonPath expressions, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
|
|
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
mockMvc.get("/people") {
|
|
accept(MediaType.APPLICATION_JSON)
|
|
}.andExpect {
|
|
jsonPath("$.links[?(@.rel == 'self')].href") {
|
|
value("http://localhost:8080/people")
|
|
}
|
|
}
|
|
----
|
|
|
|
When XML response content contains hypermedia links created with
|
|
https://github.com/spring-projects/spring-hateoas[Spring HATEOAS], you can verify the
|
|
resulting links by using XPath expressions:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
|
|
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
|
|
.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val ns = mapOf("ns" to "http://www.w3.org/2005/Atom")
|
|
mockMvc.get("/handle") {
|
|
accept(MediaType.APPLICATION_XML)
|
|
}.andExpect {
|
|
xpath("/person/ns:link[@rel='self']/@href", ns) {
|
|
string("http://localhost:8080/people")
|
|
}
|
|
}
|
|
----
|
|
|
|
[[spring-mvc-test-async-requests]]
|
|
== Async Requests
|
|
|
|
This section shows how to use MockMvc on its own to test asynchronous request handling.
|
|
If using MockMvc through the <<webtestclient>>, there is nothing special to do to make
|
|
asynchronous requests work as the `WebTestClient` automatically does what is described
|
|
in this section.
|
|
|
|
Servlet asynchronous requests, <<web.adoc#mvc-ann-async,supported in Spring MVC>>,
|
|
work by exiting the Servlet container thread and allowing the application to compute
|
|
the response asynchronously, after which an async dispatch is made to complete
|
|
processing on a Servlet container thread.
|
|
|
|
In Spring MVC Test, async requests can be tested by asserting the produced async value
|
|
first, then manually performing the async dispatch, and finally verifying the response.
|
|
Below is an example test for controller methods that return `DeferredResult`, `Callable`,
|
|
or reactive type such as Reactor `Mono`:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.*
|
|
|
|
@Test
|
|
void test() throws Exception {
|
|
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
|
|
.andExpect(status().isOk()) <1>
|
|
.andExpect(request().asyncStarted()) <2>
|
|
.andExpect(request().asyncResult("body")) <3>
|
|
.andReturn();
|
|
|
|
this.mockMvc.perform(asyncDispatch(mvcResult)) <4>
|
|
.andExpect(status().isOk()) <5>
|
|
.andExpect(content().string("body"));
|
|
}
|
|
----
|
|
<1> Check response status is still unchanged
|
|
<2> Async processing must have started
|
|
<3> Wait and assert the async result
|
|
<4> Manually perform an ASYNC dispatch (as there is no running container)
|
|
<5> Verify the final response
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
@Test
|
|
fun test() {
|
|
var mvcResult = mockMvc.get("/path").andExpect {
|
|
status { isOk() } // <1>
|
|
request { asyncStarted() } // <2>
|
|
// TODO Remove unused generic parameter
|
|
request { asyncResult<Nothing>("body") } // <3>
|
|
}.andReturn()
|
|
|
|
|
|
mockMvc.perform(asyncDispatch(mvcResult)) // <4>
|
|
.andExpect {
|
|
status { isOk() } // <5>
|
|
content().string("body")
|
|
}
|
|
}
|
|
----
|
|
<1> Check response status is still unchanged
|
|
<2> Async processing must have started
|
|
<3> Wait and assert the async result
|
|
<4> Manually perform an ASYNC dispatch (as there is no running container)
|
|
<5> Verify the final response
|
|
|
|
|
|
[[spring-mvc-test-vs-streaming-response]]
|
|
== Streaming Responses
|
|
|
|
The best way to test streaming responses such as Server-Sent Events is through the
|
|
<<WebTestClient>> which can be used as a test client to connect to a `MockMvc` instance
|
|
to perform tests on Spring MVC controllers without a running server. For example:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build();
|
|
|
|
FluxExchangeResult<Person> exchangeResult = client.get()
|
|
.uri("/persons")
|
|
.exchange()
|
|
.expectStatus().isOk()
|
|
.expectHeader().contentType("text/event-stream")
|
|
.returnResult(Person.class);
|
|
|
|
// Use StepVerifier from Project Reactor to test the streaming response
|
|
|
|
StepVerifier.create(exchangeResult.getResponseBody())
|
|
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
|
|
.expectNextCount(4)
|
|
.consumeNextWith(person -> assertThat(person.getName()).endsWith("7"))
|
|
.thenCancel()
|
|
.verify();
|
|
----
|
|
|
|
`WebTestClient` can also connect to a live server and perform full end-to-end integration
|
|
tests. This is also supported in Spring Boot where you can
|
|
{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server].
|
|
|
|
|
|
[[spring-mvc-test-server-filters]]
|
|
== Filter Registrations
|
|
|
|
When setting up a `MockMvc` instance, you can register one or more Servlet `Filter`
|
|
instances, as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
|
|
----
|
|
|
|
Registered filters are invoked through the `MockFilterChain` from `spring-test`, and the
|
|
last filter delegates to the `DispatcherServlet`.
|
|
|
|
|
|
[[spring-mvc-test-vs-end-to-end-integration-tests]]
|
|
== MockMvc vs End-to-End Tests
|
|
|
|
MockMVc is built on Servlet API mock implementations from the
|
|
`spring-test` module and does not rely on a running container. Therefore, there are
|
|
some differences when compared to full end-to-end integration tests with an actual
|
|
client and a live server running.
|
|
|
|
The easiest way to think about this is by starting with a blank `MockHttpServletRequest`.
|
|
Whatever you add to it is what the request becomes. Things that may catch you by surprise
|
|
are that there is no context path by default; no `jsessionid` cookie; no forwarding,
|
|
error, or async dispatches; and, therefore, no actual JSP rendering. Instead,
|
|
"`forwarded`" and "`redirected`" URLs are saved in the `MockHttpServletResponse` and can
|
|
be asserted with expectations.
|
|
|
|
This means that, if you use JSPs, you can verify the JSP page to which the request was
|
|
forwarded, but no HTML is rendered. In other words, the JSP is not invoked. Note,
|
|
however, that all other rendering technologies that do not rely on forwarding, such as
|
|
Thymeleaf and Freemarker, render HTML to the response body as expected. The same is true
|
|
for rendering JSON, XML, and other formats through `@ResponseBody` methods.
|
|
|
|
Alternatively, you may consider the full end-to-end integration testing support from
|
|
Spring Boot with `@SpringBootTest`. See the
|
|
{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide].
|
|
|
|
There are pros and cons for each approach. The options provided in Spring MVC Test are
|
|
different stops on the scale from classic unit testing to full integration testing. To be
|
|
certain, none of the options in Spring MVC Test fall under the category of classic unit
|
|
testing, but they are a little closer to it. For example, you can isolate the web layer
|
|
by injecting mocked services into controllers, in which case you are testing the web
|
|
layer only through the `DispatcherServlet` but with actual Spring configuration, as you
|
|
might test the data access layer in isolation from the layers above it. Also, you can use
|
|
the stand-alone setup, focusing on one controller at a time and manually providing the
|
|
configuration required to make it work.
|
|
|
|
Another important distinction when using Spring MVC Test is that, conceptually, such
|
|
tests are the server-side, so you can check what handler was used, if an exception was
|
|
handled with a HandlerExceptionResolver, what the content of the model is, what binding
|
|
errors there were, and other details. That means that it is easier to write expectations,
|
|
since the server is not an opaque box, as it is when testing it through an actual HTTP
|
|
client. This is generally an advantage of classic unit testing: It is easier to write,
|
|
reason about, and debug but does not replace the need for full integration tests. At the
|
|
same time, it is important not to lose sight of the fact that the response is the most
|
|
important thing to check. In short, there is room here for multiple styles and strategies
|
|
of testing even within the same project.
|
|
|
|
[[spring-mvc-test-server-resources]]
|
|
== Further Examples
|
|
|
|
The framework's own tests include
|
|
{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples[
|
|
many sample tests] intended to show how to use MockMvc on its own or through the
|
|
{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client[
|
|
WebTestClient]. Browse these examples for further ideas.
|
|
|
|
|
|
[[spring-mvc-test-server-htmlunit]]
|
|
== HtmlUnit Integration
|
|
|
|
Spring provides integration between <<spring-mvc-test-server, MockMvc>> and
|
|
https://htmlunit.sourceforge.io/[HtmlUnit]. This simplifies performing end-to-end testing
|
|
when using HTML-based views. This integration lets you:
|
|
|
|
* Easily test HTML pages by using tools such as
|
|
https://htmlunit.sourceforge.io/[HtmlUnit],
|
|
https://www.seleniumhq.org[WebDriver], and
|
|
https://www.gebish.org/manual/current/#spock-junit-testng[Geb] without the need to
|
|
deploy to a Servlet container.
|
|
* Test JavaScript within pages.
|
|
* Optionally, test using mock services to speed up testing.
|
|
* Share logic between in-container end-to-end tests and out-of-container integration tests.
|
|
|
|
NOTE: MockMvc works with templating technologies that do not rely on a Servlet Container
|
|
(for example, Thymeleaf, FreeMarker, and others), but it does not work with JSPs, since
|
|
they rely on the Servlet container.
|
|
|
|
[[spring-mvc-test-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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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"));
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
@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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
mockMvc.perform(get("/messages/form"))
|
|
.andExpect(xpath("//input[@name='summary']").exists())
|
|
.andExpect(xpath("//textarea[@name='text']").exists());
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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 synch with the controller. To resolve this we can combine our two tests, as
|
|
follows:
|
|
|
|
[[spring-mvc-test-server-htmlunit-mock-mvc-test]]
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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"));
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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.
|
|
|
|
[[spring-mvc-test-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.
|
|
|
|
[[spring-mvc-test-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.`"
|
|
|
|
[[spring-mvc-test-server-htmlunit-options]]
|
|
==== HtmlUnit Integration Options
|
|
|
|
You have a number of options when you want to integrate MockMvc with HtmlUnit:
|
|
|
|
* <<spring-mvc-test-server-htmlunit-mah,MockMvc and HtmlUnit>>: Use this option if you
|
|
want to use the raw HtmlUnit libraries.
|
|
* <<spring-mvc-test-server-htmlunit-webdriver,MockMvc and WebDriver>>: Use this option to
|
|
ease development and reuse code between integration and end-to-end testing.
|
|
* <<spring-mvc-test-server-htmlunit-geb,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.
|
|
|
|
[[spring-mvc-test-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.
|
|
|
|
[[spring-mvc-test-server-htmlunit-mah-setup]]
|
|
==== MockMvc and HtmlUnit Setup
|
|
|
|
First, make sure that you have included a test dependency on
|
|
`net.sourceforge.htmlunit:htmlunit`. In order to use HtmlUnit with Apache HttpComponents
|
|
4.5+, you need to use HtmlUnit 2.18 or higher.
|
|
|
|
We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the
|
|
`MockMvcWebClientBuilder`, as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient webClient;
|
|
|
|
@BeforeEach
|
|
void setup(WebApplicationContext context) {
|
|
webClient = MockMvcWebClientBuilder
|
|
.webAppContextSetup(context)
|
|
.build();
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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 <<spring-mvc-test-server-htmlunit-mah-advanced-builder>>.
|
|
|
|
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.
|
|
|
|
[[spring-mvc-test-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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
|
|
----
|
|
|
|
NOTE: The default context path is `""`. Alternatively, we can specify the context path,
|
|
as described in <<spring-mvc-test-server-htmlunit-mah-advanced-builder>>.
|
|
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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();
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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 https://assertj.github.io/doc/[AssertJ] library:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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!");
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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
|
|
<<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.
|
|
|
|
[[spring-mvc-test-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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebClient webClient;
|
|
|
|
@BeforeEach
|
|
void setup(WebApplicationContext context) {
|
|
webClient = MockMvcWebClientBuilder
|
|
.webAppContextSetup(context)
|
|
.build();
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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();
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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();
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/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
|
|
<<spring-mvc-test-server-setup-options>>.
|
|
|
|
[[spring-mvc-test-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.
|
|
|
|
[[spring-mvc-test-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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
|
|
summaryInput.setValueAttribute(summary);
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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);
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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());
|
|
}
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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.
|
|
|
|
[[spring-mvc-test-server-htmlunit-webdriver-setup]]
|
|
==== MockMvc and WebDriver Setup
|
|
|
|
To use Selenium WebDriver with the Spring MVC Test framework, make sure that your project
|
|
includes a test dependency on `org.seleniumhq.selenium:selenium-htmlunit-driver`.
|
|
|
|
We can easily create a Selenium WebDriver that integrates with MockMvc by using the
|
|
`MockMvcHtmlUnitDriverBuilder` as the following example shows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebDriver driver;
|
|
|
|
@BeforeEach
|
|
void setup(WebApplicationContext context) {
|
|
driver = MockMvcHtmlUnitDriverBuilder
|
|
.webAppContextSetup(context)
|
|
.build();
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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 <<spring-mvc-test-server-htmlunit-webdriver-advanced-builder>>.
|
|
|
|
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.
|
|
|
|
[[spring-mvc-test-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:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
CreateMessagePage page = CreateMessagePage.to(driver);
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val page = CreateMessagePage.to(driver)
|
|
----
|
|
--
|
|
|
|
We can then fill out the form and submit it to create a message, as follows:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
ViewMessagePage viewMessagePage =
|
|
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
val viewMessagePage =
|
|
page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)
|
|
----
|
|
--
|
|
|
|
This improves on the design of our <<spring-mvc-test-server-htmlunit-mah-usage, HtmlUnit test>>
|
|
by leveraging the Page Object Pattern. As we mentioned in
|
|
<<spring-mvc-test-server-htmlunit-webdriver-why>>, we can use the Page Object Pattern
|
|
with HtmlUnit, but it is much easier with WebDriver. Consider the following
|
|
`CreateMessagePage` implementation:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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]`).
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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 https://assertj.github.io/doc/[AssertJ] assertion library:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
|
|
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
public Message getMessage() throws ParseException {
|
|
Message message = new Message();
|
|
message.setId(getId());
|
|
message.setCreated(getCreated());
|
|
message.setSummary(getSummary());
|
|
message.setText(getText());
|
|
return message;
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
--
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
@AfterEach
|
|
void destroy() {
|
|
if (driver != null) {
|
|
driver.close();
|
|
}
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
@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].
|
|
|
|
[[spring-mvc-test-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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
WebDriver driver;
|
|
|
|
@BeforeEach
|
|
void setup(WebApplicationContext context) {
|
|
driver = MockMvcHtmlUnitDriverBuilder
|
|
.webAppContextSetup(context)
|
|
.build();
|
|
}
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
lateinit var driver: WebDriver
|
|
|
|
@BeforeEach
|
|
fun setup(context: WebApplicationContext) {
|
|
driver = MockMvcHtmlUnitDriverBuilder
|
|
.webAppContextSetup(context)
|
|
.build()
|
|
}
|
|
----
|
|
|
|
We can also specify additional configuration options, as follows:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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();
|
|
}
|
|
----
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
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:
|
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
|
.Java
|
|
----
|
|
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();
|
|
----
|
|
|
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
|
.Kotlin
|
|
----
|
|
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/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
|
|
<<spring-mvc-test-server-setup-options>>.
|
|
|
|
[[spring-mvc-test-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.
|
|
|
|
[[spring-mvc-test-server-htmlunit-geb-why]]
|
|
==== Why Geb and MockMvc?
|
|
|
|
Geb is backed by WebDriver, so it offers many of the
|
|
<<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.
|
|
|
|
[[spring-mvc-test-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 <<spring-mvc-test-server-htmlunit-webdriver-advanced-builder>>.
|
|
|
|
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.
|
|
|
|
[[spring-mvc-test-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
|
|
<<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.
|