Spring MVC Test with WebDriver

Engineering | Rob Winch | March 26, 2014 | ...

In my second post I described how to use Spring MVC Test with HtmlUnit. In this post we will leverage additional abstractions within WebDriver to make things even easier.

Why WebDriver?

We can already use HtmlUnit and MockMvc, so why would we want to use WebDriver? WebDriver provides a very elegant API and allows us to easily organize our code. To better understand, let's explore an example.


NOTE Despite being a part of 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 inputs, filling them out, and making various assertions.

There are many tests because we want to test error conditions as well. For example, we want to ensure that if we fill out only part of the form we get an error. If we fill out the entire form, the newly created message is displayed afterwards.

If one of the fields was named "summary", then we might have something like the following repeated everywhere within our tests:

HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");

So what happens if we change the id to be "smmry". This means we would have to update all of our tests! Instead we would hope that we wrote a bit more elegant code where filling out the form was in its own method:

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);
}

This ensures that if we change the UI we do not have to update all of our tests.

We might take it a step further and place this logic within an Object that represents the HtmlPage we are currently on.

public class CreateMessagePage {
  private final HtmlPage currentPage;

  ...

  public T createMessage(Class<T> resultPage, String summary, String text) {
    ...
    setSummary(currentPage, summary);
    ...
    HtmlPage result = submit.click();
    ...
    return (T) error ? new CreateMessagePage(result) : new ViewMessagePage(result);
  }

  public void setSummary(String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
  }
}

Formerly, this pattern is known as the Page Object Pattern. While we can certainly do this with HtmlUnit, WebDriver provides some tools that we will explore in the following sections make this pattern much easier.

Updating Dependencies

Before you use the project, you must ensure to update your dependencies. Instructions for both Maven and Gradle can be found on the site documentation.

Using WebDriver

Now that we have the correct dependencies, we can use WebDriver in our unit tests. Our example assumes you already have JUnit as a dependency. If you have not added it, please update your classpath accordingly. The complete code sample for using WebDriver and Spring MVC Test can be found in MockMvcHtmlUnitDriverCreateMessageTests.

Creating MockMvc

In order to use WebDriver and Spring MVC Test we must first create a MockMvc instance. There is plenty of documentation on how to create a MockMvc instance, but we will review how to create a MockMvc instance very quickly in this section.

The first step is to create a new JUnit class that is annotated as shown below:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {WebMvcConfig.class, MockDataConfig.class})
@WebAppConfiguration
public class MockMvcHtmlUnitDriverCreateMessageTests {

  @Autowired
  private WebApplicationContext context;

  ...
}
  • @RunWith(SpringJUnit4ClassRunner.class) allows Spring to perform dependency injection on our MockMvcHtmlUnitDriverCreateMessageTests. This is why our @Autowired annotations will be honored.
  • @ContextConfiguration tells Spring what configuration to load. You will notice that we are loading a mock instance of our data tier to improve the performance of our tests. If we wanted, we could optionally run the tests against a real database. However, this has the disadvantages we mentioned previously.
  • @WebAppConfiguration indicates to SpringJUnit4ClassRunner that it should create a WebApplicationContext rather than a ApplicationContext.

Next we need to create our MockMvc instance from the context. An example of how to do this has been provided below:

@Before
public void setup() {
  MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
  ...  
}

Of course this is just one way to create a MockMvc instance. We could have decided to add a Servlet Filter, use a Standalone setup, etc. The important thing is that we need an instance of MockMvc. For additional information on creating a MockMvc instance refer to the Spring MVC Test documentation.

Initializing WebDriver

Now that we have created the MockMvc instance, we need to create a MockMvcHtmlUnitDriver which ensures we use the MockMvc instance we created in the previous step.

private WebDriver driver;

@Before
public void setup() {
	MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
	driver = new MockMvcHtmlUnitDriver(mockMvc, true);
}

Using WebDriver

Now we can use WebDriver as we normally would, but without the need to deploy our application. For example, we can request the view to create a message with the following:

CreateMessagePage messagePage = CreateMessagePage.to(driver);

We can then fill out the form and submit it to create a message.

ViewMessagePage viewMessagePage = 
    messagePage.createMessage(ViewMessagePage.class, expectedSummary, expectedText);

This improves on the design of our HtmlUnit test by leveraging the Page Object Pattern. As we mentioned in Why WebDriver?, we could use the Page Object Pattern with HtmlUnit, but it is much easier now. Let's take a look at our 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);
    }
}

The first thing you will notice is that our CreateMessagePage extends the AbstractPage. We won't go over the details of AbstractPage, but in summary it contains all the common functionality of all our pages. For example, if your application has a navigational bar, global error messages, etc. This logic can be placed in a shared location.

The next thing you will find is that we have a member variable for each of the parts of the HTML, WebElement, we are interested in. WebDriver's PageFactory allows us to remove a lot of code from HtmlUnit version of CreateMessagePage by automatically resolving each WebElement.

The PageFactory#initElements method will automatically resolve each WebElement by using the field name and trying to look it up by id or name of the element on the HTML page. We can also use the @FindBy annotation to override the default. Our example demonstrates how we can use the @FindBy annotation to lookup our submit button using the css selector of input[type=submit].

Finally, we can verify that a new message was created successfully

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");

We can see that our ViewMessagePage can return a Message object in addition to the individual Message properties. This allows us to easily interact with our rich domain objects instead of just a String. We can then leverage the rich domain objects in our assertions. We do this by creating a custom fest assertion that allows us to verify all the properties of the actual Message are equal to the expected Message. You can view the details of the custom assertion in Assertions and MessageAssert

Last, don't forget to close the WebDriver instance when we are done.

@After
public void destroy() {
	if(driver != null) {
		driver.close();
	}
}

For additional information on using WebDriver, refer to the WebDriver documentation.

Making it all Groovy...

WebDriver has the same benefits of using HtmlUnit with the added benefit of easy support of using the Page Object pattern. However, there is quite a bit of boiler plate code that could be improved on. In our next post, we will see how we can use Geb to make our tests more Groovy.


Feedback please!

If you have feedback on this blog series or the Spring Test MVC HtmlUnit, I encourage you to reach out via github issues or ping me on twitter @rob_winch. Of course the best feedback comes in the form of contributions.

Get the Spring newsletter

Stay connected with the Spring newsletter

Subscribe

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all