You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1874 lines
64 KiB
1874 lines
64 KiB
[[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.
|
|
|