Spring provides integration between MockMvc and HtmlUnit. This makes it easier to perform end-to-end testing when using HTML-based views. This integration allows you to:
Easily test HTML pages using tools like HtmlUnit, WebDriver and Geb, without the need to deploy to a servlet container.
Test JavaScript inside pages.
Optionally use mock services to speed up testing.
Ensure that end-to-end tests in the container and integration tests outside the container share logic.
Why is HtmlUnit integration needed?
The most obvious question that comes to mind is: “Why is it
needed?”. The easiest way to find the answer is to study a very clear example application. Let's assume that you
have a Spring MVC web application that supports CRUD operations on a Message
object. The application
also supports paging of all messages. How to test it?
Using Spring MVC Test, we can easily check if we can
create a Message
, like this:
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"));
@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 need to test a form submission that allows us to create a message? For example, let's say our form looks like the following snippet:
<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 can we make our form prompt correctly to create a new message? A naive attempt might look like this:
mockMvc.perform(get("/messages/form"))
.andExpect(xpath("//input[@name='summary']").exists())
.andExpect(xpath("//textarea[@name='text']").exists());
mockMvc.get("/messages/form").andExpect {
xpath("//input[@name='summary']") { exists() }
xpath("//textarea[@name='text']") { exists() }
}
This test has a few obvious shortcomings. If we update our controller to use the message
parameter instead of text
, our form test will pass even if the HTML form is out of sync with the
controller. To solve this problem, we can combine our two tests as follows:
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"));
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 will reduce the risk of our test passing incorrectly, but there are still some problems:
What to do if there are several forms on the page? Of course, we can update our XPath expressions, but they become increasingly complex as we consider more factors: Are the fields the right type? Are the fields enabled? And so on.
Another problem is that we are doing twice as much work as expected. We need to first validate the view and then submit that view with the same parameters we just validated. Ideally, all this can be done at once.
Finally, there are still some things we can't take into account. For example, what if the form has JavaScript validation that we also want to test?
A common problem is that testing a web page does not involve a one-time interaction. Instead, it is a combination of how a user interacts with a web page and how that web page interacts with other resources. For example, the result of a form submission is used as input for the user to create a message. In addition, our form submission can potentially use additional resources that affect the logic of the page, such as JavaScript validation.
Integration testing to help?
To solve the problems mentioned above, we can perform end-to-end integration testing, but it has several disadvantages. Let's look at testing a view that allows us to view messages. We may need to test the following:
Does our page display a notification to the user that there are no results if posts are empty?
Does our page properly display one single message?
Does our page properly support paging?
To set up these tests, we need to make sure that our database contains the necessary messages. This introduces a number of additional problems:
Ensuring that the correct messages are in the database can be a time-consuming process. (Given foreign key constraints).
Testing can be slow because each test needs to ensure that the database is in the correct state.
Because our database must be in a certain state, we cannot run tests in parallel.
Running assertions on items such as auto-generated IDs, timestamps, etc. can be difficult.
These problems do not mean that end-to-end integration testing should be abandoned completely. Instead, we can reduce the number of end-to-end integration tests by refactoring our detailed tests to use mock services that are much faster, more reliable, and without side effects. You can then run a small number of true end-to-end integration tests that test simple workflows to make sure everything works as expected.
Introducing HtmlUnit Integration
How do we strike a balance between testing how our pages and maintaining proper performance in our test suite? Answer: "By integrating MockMvc with HtmlUnit."
HtmlUnit Integration Options
If you need to integrate MockMvc with HtmlUnit, there are several options:
MockMvc and HtmlUnit: Use this option if you want to use raw HtmlUnit libraries.
MockMvc and WebDriver: Use this option to facilitate code development and reuse between integration and end-to-end testing phases.
MockMvc and Geb: Use this option if you need to use Groovy for testing, make it easier development and reuse code between the integration and end-to-end testing phases.
MockMvc and HtmlUnit
This section describes how to integrate MockMvc and HtmlUnit. Use this option if you want to use the raw HtmlUnit libraries.
Configuring MockMvc and HtmlUnit
First, make sure you
have activated the test dependency on net.sourceforge.htmlunit:htmlunit
. To use HtmlUnit with Apache
HttpComponents 4.5+, you must use HtmlUnit 2.18 or higher.
You can easily create a WebClient
from HtmlUnit that will be integrated with MockMvc using MockMvcWebClientBuilder
as shown below:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
MockMvcWebClientBuilder
. For
advanced usage, see "Advanced usage ofMockMvcWebClientBuilder"
.
This way we can ensure that any URL referencing localhost
as the server will be directed to our
MockMvc
instance without the need for actual HTTP - connection. Requesting any other URL occurs through
the network connection as usual. This allows us to easily test the use of content delivery (and distribution)
networks (CDNs).
Using MockMvc and HtmlUnit
Now we can use HtmlUnit as standard, but without the need to deploy our application in a servlet container. For example, you could ask the view to create a message like this:
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
""
. You can also specify the path to
the context, as described in "Advanced Usage of
MockMvcWebClientBuilder
".
Once we have a link to HtmlPage
, we can
immediately fill out the form and submit it to create a message, as shown in the following example:
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();
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, you can make sure that a new message was successfully created. The following assertions use the AssertJ library:
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!");
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 previous code improves our test MockMvc in several ways. First, we no longer need to explicitly validate our form and then create a request that looks like a form. Instead, we request the form, fill it out and submit it, thereby significantly reducing processing delay.
Another important factor is that HtmlUnit uses the Mozilla Rhinoengine to calculate the output of JavaScript. This means we can also test the JavaScript logic on our pages.
More information about using HtmlUnit See documentation on using HtmlUnit.
Advanced using MockMvcWebClientBuilder
So far in the examples we have used MockMvcWebClientBuilder
in the simplest way, creating a WebClient
based on a WebApplicationContext
, loaded for us
by the Spring TestContext Framework. This approach is repeated in the following example:
WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build();
}
lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
webClient = MockMvcWebClientBuilder
.webAppContextSetup(context)
.build()
}
We can also specify additional configuration options, as shown in the following example:
WebClient webClient;
@BeforeEach
void setup() {
webClient = MockMvcWebClientBuilder
// demonstrates the use of MockMvcConfigurer (Spring Security) .
webAppContextSetup(context, springSecurity())
// for example only - default ""
.contextPath("")
// MockMvc is used by default only for localhost;
// next part will use MockMvc also for example.com and example.org
.useMockMvcForHosts("example.com","example.org")
.build();
}
lateinit var webClient: WebClient
@BeforeEach
fun setup() {
webClient = MockMvcWebClientBuilder
// demonstrates using MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for example only - default ""
.contextPath("")
// By default MockMvc is used for localhost only;
// next part will use MockMvc also for example.com and example.org
.useMockMvcForHosts("example.com","example.org")
.build()
}
Alternatively, you can do the exact same setup by configuring a MockMvc
instance separately and
passing it to the MockMvcWebClientBuilder
as shown below:
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
webClient = MockMvcWebClientBuilder
.mockMvcSetup(mockMvc)
// for example only - default ""
.contextPath("")
// By default MockMvc is used for localhost only;
// next part will use MockMvc also for example.com and example.org
.useMockMvcForHosts("example.com","example.org")
.build();
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
This is more overloaded code, but by creating a WebClient
using a MockMvc
instance,
we have all the functionality of MockMvc at our disposal.
MockMvc
can be found in "Settings
options".
MockMvc and WebDriver
In the previous sections, we learned about using MockMvc in combination with the raw HtmlUnit APIs. This section uses additional abstractions in Selenium WebDriver, which makes everything even simpler.
Why WebDriver and MockMvc?
We can already use HtmlUnit and MockMvc, so why do we need WebDriver? Selenium WebDriver provides an extremely elegant API that will allow us to easily organize our code. To better show how this works, we'll look at an example in this section.
Let's say we need to make sure that the message is created correctly. The tests involve finding HTML form input elements, filling them out, and executing various assertions.
This approach results in many separate tests because we need to check error conditions as well. For example, we want an error to be thrown when filling out only part of the form. If we fill out the entire form, a newly created message should appear afterwards.
If one of the fields were named "summary", we could end up with something similar to the one below, repeated in several places in our tests:
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput .setValueAttribute(summary)
What happens if we change id
to smmry
? This will force us to update all our tests to
take this change into account. This violates the Don't Repeat Yourself (DRY) principle, so ideally we should
separate this code into a separate method, as shown below:
public HtmlPage createMessage( HtmlPage currentPage, String summary, String text) {
setSummary(currentPage, summary);
// ...
}
public void setSummary(HtmlPage currentPage, String summary) {
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
}
fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
setSummary(currentPage, summary);
// ...
}
fun setSummary(currentPage:HtmlPage , summary: String) {
val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput.setValueAttribute(summary)
}
This ensures that we don't have to update all our tests if we change the UI.
We can even go one step
further and put this logic in a Object
that represents the HtmlPage
that we are currently
on, as shown in the following example:
public class CreateMessagePage {
final HtmlPage currentPage;
final HtmlTextInput summaryInput;
final HtmlSubmitInput submit;
public CreateMessagePage(HtmlPage currentPage) {
this.currentPage = currentPage;
this.summaryInput = currentPage.getHtmlElementById("summary");
this.submit = currentPage.getHtmlElementById("submit");
}
public <T> T createMessage(String summary, String text) throws Exception {
setSummary(summary);
HtmlPage result = submit.click();
boolean error = CreateMessagePage.at(result);
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
}
public void setSummary(String summary) throws Exception {
summaryInput.setValueAttribute(summary);
}
public static boolean at(HtmlPage page) {
return "Create Message".equals(page.getTitleText());
}
}
class CreateMessagePage(private val currentPage: HtmlPage) {
val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")
val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")
fun <T> createMessage(summary: String, text: String): T {
setSummary(summary)
val result = submit.click()
val error = at(result)
return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
}
fun setSummary(summary: String) {
summaryInput.setValueAttribute(summary)
}
fun at(page: HtmlPage): Boolean {
return "Create Message" == page.getTitleText()
}
}
}
This pattern was previously known as the (Page Object Pattern). Of course, you can do this with HtmlUnit, but WebDriver provides some tools that make implementing this pattern much easier, and we'll look at them in the following sections.
Configuring MockMvc and WebDriver
To use Selenium WebDriver with the Spring MVC Test framework, make sure your project contains a test dependency
on org.seleniumhq.selenium:selenium-htmlunit-driver
.
You can easily create a Selenium WebDriver that integrates with MockMvc using
MockMvcHtmlUnitDriverBuilder
, as shown in the following example:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
MockMvcHtmlUnitDriverBuilder
.
For advanced usage, see "
Advanced use of MockMvcHtmlUnitDriverBuilder"
.
In the previous example, any URL referencing localhost
as the server is guaranteed to be directed
to our MockMvc
instance without the need for an actual HTTP connection . Requesting any other URL
occurs through the network connection as usual. This allows us to easily test the use of content delivery (and
distribution) networks (CDNs).
Using MockMvc and WebDriver
We can now use WebDriver as standard, but without the need to deploy our application in a container servlets. For example, you could ask the view to create a message like this:
CreateMessagePage page = CreateMessagePage.to(driver);
val page = CreateMessagePage.to(driver)
Then you can fill out the form and submit it to create a message, as follows:
ViewMessagePage viewMessagePage =
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
val viewMessagePage =
page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)
This will improve the structure of our HtmlUnit
test by using the Page Object Pattern. As already mentioned in the subsection "Why
WebDriver and MockMvc? "We can use the Page Object Pattern with HtmlUnit, but with WebDriver it's still easier.
Consider the following implementation of CreateMessagePage
:
public class CreateMessagePage
extends AbstractPage {
private WebElement summary;
private WebElement text;
@FindBy(css = "input[type=submit]")
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);
}
}
CreateMessagePage
extendsAbstractPage
. We won't go into detail aboutAbstractPage
, but in short, it contains common functionality for all our pages. For example, if our application has a navigation bar, global error messages, and other functionality, we can place this logic in a common place.- We have a member variable for each part of the HTML page that we are interested in . They are of type
WebElement
.PageFactory
from WebDriver allows us to remove a lot of unnecessary code from HtmlUnit- version ofCreateMessagePage
, automatically resolving eachWebElement
. MethodPageFactory#initElements(WebDriver,Class<T>)
automatically resolves eachWebElement
using the field name and searching for it byid
orname
element in the HTML page. - You can use
@FindBy
annotation to override the default search logic. Our example shows how to use the@FindBy
annotation to find our submit button using thecss
(input[type=submit]) selector.
class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) {
private lateinit var summary: WebElement
private lateinit var text: WebElement
@FindBy(css = "input[type=submit]")
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)
}
}
}
CreateMessagePage
extendsAbstractPage
. We won't go into detail aboutAbstractPage
, but in short, it contains common functionality for all our pages. For example, if our application has a navigation bar, global error messages, and other functionality, we can place this logic in a common place.- We have a member variable for each part of the HTML page that we are interested in . They are of type
WebElement
.PageFactory
from WebDriver allows us to remove a lot of unnecessary code from HtmlUnit- version ofCreateMessagePage
, automatically resolving eachWebElement
. MethodPageFactory#initElements(WebDriver,Class<T>)
automatically resolves eachWebElement
using the field name and searching for it byid
orname
element in the HTML page. - You can use
@FindBy
annotation to override the default search logic. Our example shows how to use the@FindBy
annotation to find our submit button using thecss
(input[type=submit]) selector.
Finally, you can verify that the new message was successfully created. The following assertions use the AssertJ assertion library:
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")
We see that our ViewMessagePage
page allows us to interact with our custom domain model. For
example, it opens a method that returns a Message
object:
public Message getMessage() throws ParseException {
Message message = new Message ();
message.setId(getId());
message.setCreated(getCreated());
message.setSummary(getSummary());
message.setText(getText());
return message;
}
fun getMessage() = Message(getId(), getCreated(), getSummary() , getText())
We can then use full-featured domain objects in our statements.
Finally, don't forget to close the
WebDriver
instance when the test is completed, as shown below:
@AfterEach
void destroy() {
if (driver != null) {
driver .close();
}
}
@AfterEach
fun destroy() {
if (driver != null) {
driver. close()
}
}
For more information on using WebDriver, see the Selenium documentation WebDriver.
Advanced use of MockMvcHtmlUnitDriverBuilder
So far in the examples we used MockMvcHtmlUnitDriverBuilder
in the simplest way possible, creating a WebDriver
based on the WebApplicationContext
loaded for us by the Spring TestContext Framework. This approach is repeated here as follows:
WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build();
}
lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
We can also specify additional configuration options as shown below:
WebDriver driver;
@BeforeEach
void setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates the use of MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity()) // for example only - default ""
.contextPath("")
// MockMvc is used by default only for localhost;
// next part will use MockMvc also for example.com and example.org
.useMockMvcForHosts("example.com","example.org")
.build();
}
lateinit var driver: WebDriver
@BeforeEach
fun setup() {
driver = MockMvcHtmlUnitDriverBuilder
// demonstrates using MockMvcConfigurer (Spring Security)
.webAppContextSetup(context, springSecurity())
// for example only - default ""
.contextPath("")
// By default MockMvc is used for localhost only;
// next part will use MockMvc also for example.com and example.org
.useMockMvcForHosts("example.com","example.org")
.build()
}
Alternatively, you can do the exact same setup by configuring the MockMvc
instance separately
and passing it to the MockMvcHtmlUnitDriverBuilder
like this:
MockMvc mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
driver = MockMvcHtmlUnitDriverBuilder
.mockMvcSetup(mockMvc)
// for example only - default ""
.contextPath("")
// By default MockMvc is used only for localhost;
// next part will use MockMvc also for example.com and example.org
.useMockMvcForHosts("example.com","example.org")
.build();
// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed
This is more overloaded code, but by creating a WebDriver
using a MockMvc
instance,
we have all the functionality of MockMvc at our disposal.
MockMvc
can be found in "Settings
options".
MockMvc and Geb
In the previous section, we looked at how to use MockMvc with WebDriver. In this section, we use Geb to make our tests even more Groovy-centric.
Why Geb and MockMvc?
Geb is supported by WebDriver, so it provides many of the same benefits which we receive from WebDriver. However, Geb makes things even easier by running the boilerplate code for us.
Configuring MockMvc and Geb
You can
easily initialize Browser
from Geb with Selenium WebDriver, which uses MockMvc, as follows:
def setup() {
browser.driver = MockMvcHtmlUnitDriverBuilder
.webAppContextSetup(context)
.build()
}
MockMvcHtmlUnitDriverBuilder
.
For advanced usage, see "
Advanced use of MockMvcHtmlUnitDriverBuilder"
.
This way we can ensure that any URL referencing localhost
as the server will be directed to our
MockMvc
instance without the need for actual HTTP - connection. Requesting any other URL occurs through
the network connection as usual. This allows us to easily test the use of content delivery (and distribution)
networks (CDNs).
Using MockMvc and Geb
We can now use Geb as standard, but without the need to deploy our application in a container servlets. For example, you could ask the view to create a message like this:
to CreateMessagePage
You can then fill out the form and submit it to create the message as follows:
when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click( ViewMessagePage)
Any unrecognized method calls, property calls, or references that were not found are passed to the current page object. This eliminates a lot of boilerplate code required when using WebDriver directly.
As with direct use of WebDriver, this is how we improve the structure of our HtmlUnit
test by using the Page Object Pattern. As mentioned earlier, you can use the Page Object Pattern with HtmlUnit
and WebDriver, but with Geb everything gets even more complicated. Let's take a look at our new Groovy-based CreateMessagePage
implementation:
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 AbstractPage
. We won't go into detail about Page
,
but in short, it contains common functionality for all our pages. We define the URL where this page can be found.
This allows us to navigate to the page as shown below:
to CreateMessagePage
We also have a at
closure that determines whether we are on the specified page. It should return true
if we are on the correct page. That's why we can confirm that we are on the right page as follows:
then:
at CreateMessagePage
errors.contains('This field is required.')
Next, we create a content
closure that specifies all the places of interest on the page. You can use jQuery-like navigator
API to selecting the content we are interested in.
Finally, we can verify that the new message was created successfully, as follows:
then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage
More information on how to get the most out of Geb can be found in the user manual entitled "The Book of Geb".