Integration testing: Validation and relationships
In my previous two posts about integration testing with jetty [1][2], I have showed how to test a simple web application with Jetty and how to expand this application with a fake DAO implementation to test display and updates of data through the web interface. In this penultimate post, I will describe how to work with more advanced edit scenarios.
Most web applications have some degree of requirements for validation of input and a bit more advanced data structure than the one we’ve worked with so far. I will expand the example to show how to do validation using Spring-MVC and how to handle many-to-one relationships between domain objects.
Validating input
Let’s start with something simple: A name cannot contain certain characters. Here is a unit test:
public void testEditIllegalName() {
populateTestdata();
String oldName = category.getName();
beginAt("/category/edit.html?id=" + category.getId());
setFormElement("name", "illegal + name");
submit();
// Verify that we get a decent error message
assertTextPresent("contains illegal characters");
// Verify that the form is as we left it
assertFormElementEquals("name", "illegal + name");
}
Running the test should produce a red bar. In order for this to work, we have to do something to validate. The simplest thing I can think of is to modify Category.setName
:
public void setName(String name) {
String namePattern = "[a-zA-Z0-9 ]+";
if (!name.matches(namePattern)) {
throw new IllegalArgumentException("name " + name + " contains illegal characters");
}
this.name = name;
}
At this point in time, everything works like we want it to. Spring-MVC catches the exception and displays the exception code in the corresponding @spring.showErrors tag in the view. Here’s the relevant fragment of the FreeMarker template for edit.ftl: <p>Name: <@spring.formInput "category.name" /> <@spring.showErrors "<br>"/></p>
.
More validation
Let’s do another validation example: Let’s say that the name must be unique. Here is a test for that:
public void testDuplicateNamesAreIllegal() {
String oldName = category.getName();
String newName = category2.getName();
beginAt("/category/edit.html?id=" + category.getI());
setFormElement("name", newName);
submit();
assertTextPresent("duplicate name");
assertFormElementEquals("name", newName);
}
This is hard to test in Category.setName, as we need to check what other names are out there. Enter Spring-MVC validators. If a SimpleFormController has a validator, this object gets to set validation errors before Spring-MVC calls the submit action. If the any errors are set, the submit action is not called. Instead the user will be taken back to the form page. This is just perfect for what we’re doing. Since we always want the same validator in CategoryController, I call setValidator explicitly in the constructor. This could also have been done from Spring-XML. (But I find that making configurable that which we will never configure is usually counter productive)
public CategoryController() {
setCommandName("category");
setValidator(new Validator() {
public boolean supports(Class clazz) {
return Category.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
validateNameUnique((Category)target, errors);
}
});
}
And the validation method itself:
private void validateNameUnique(Category category, Errors errors) {
List categoriesWithSameName = categoryDAO.list(category.getName());
if (categoriesWithSameName.isEmpty()) {
return;
} else if (categoriesWithSameName.size() > 1) {
// This is kind of a special situation: The data in the database must already be corrupt
// Lets just not make it worse!
errors.rejectValue("name", "duplicateName", "duplicate name");
} else {
// If we find something in the database, it should be the object we already have
// This will happen if the name is unchanged
Category matchedCategory = categoriesWithSameName.iterator().next();
if (matchedCategory.getId() != category.getId()) {
errors.rejectValue("name", "duplicateName", "duplicate name");
}
}
}
At this point, the test passes, and we’re happy. But wait, there is something lurking in the shadows….
Testing by “click-like-hell”
At this point in time, things are going a bit too well. This should always be a warning sign that we’re forgetting something. To validate what’s going on, it’s often a good idea to create a main-class to start testing your application. The maven-jetty-plugin can also be used, but the advantage of creating a main-class is that this way, we can debug natively in the IDE. Here is my application starter:
public class TestWebServer {
public static void main(String[] args) throws Exception {
Server server = new Server(9080);
server.addHandler(new WebAppContext("src/main/webapp", "/"));
try {
server.start();
setupTestData(server);
} catch (Exception e) {
server.stop();
e.printStackTrace();
}
}
private static void setupTestData(Server server) throws Exception {
CategoryDAO dao = createCategoryDao(server);
populateTestData(dao);
getWebContextBeanOfType(server, CategoryListController.class).setDao(dao);
getWebContextBeanOfType(server, CategoryController.class).setDao(dao);
}
private static void populateTestData(CategoryDAO categoryDao) {
Category parentCategory = new Category("parent category " + System.currentTimeMillis());
Category subcategory = parentCategory.addSubcategory("subcategory " + System.currentTimeMillis());
Category extraSubcategory = parentCategory.addSubcategory("extra subcategory " + System.currentTimeMillis());
categoryDao.insert(parentCategory);
categoryDao.insert(subcategory);
categoryDao.insert(extraSubcategory);
}
}
If you run this application, go to http://localhost:9080/category/index.html and click around a little, you will notice something strange: When we enter a duplicate name, even though we get the validation error, the Category is updated. Why does this happen? Before we fix the problem, let’s update testDuplicateNamesAreIllegal to reproduces it:
public void testDuplicateNamesAreIllegal() {
String oldName = category.getName();
String newName = category2.getName();
beginAt("/category/edit.html?id=" + category1.getId()));
setFormElement("name", newName);
submit();
assertTextPresent("duplicate name");
assertFormElementEquals("name", newName);
beginAt("/category/show.html?id=" + category1.getId()));
assertTextInElement("name", oldName);
}
The fake DAO is a bit too fake
The problem with using fakes is that they can be too fake. In our case, the fake DAO doesn’t preserve the semantics of the “update” method. We are storing all objects in a table, and when “get” is called, we share this object in the controller and DAO. Even if update() is never called, any changes made to the returned Category will be reflected in the DAO. At this point in time, it makes sense to start writing some tests for the DAO. This is a suspicious situation: We are testing code that was written with the purpose of helping in testing! CategoryDAOTest should better be something we can use again when we replace the fake with a real DAO implementation. Here is a first test that will fail:
public void testUnsavedChanges() {
Category category = new Category("foo");
categoryDAO.insert(category);
category = categoryDAO.get(category.getId());
category.setName("bar");
// Since we haven't called update, the value in the DAO must be the original one
assertEquals("foo", categoryDAO.get(category.getId()).getName());
}
To make it pass, simply make FakeCategoryDAO.get() return a clone of the Category. (Hint: override clone in Category with “public Category clone();” and catch CloneNotSupportedException in this method)
Redirect-on-POST
This will make the integration test pass again, and if you check the web UI, things are handled correctly now. The lesson is worth contemplating though: The fact that with a database, the objects represent a local copy of the database state makes some tests invalid if we don’t watch what we’re doing. To avoid similar problems, it can be useful to implement a Redirect-on-POST policy: Every time a user successfully posts data, instead of displaying a form, the user is redirected to a new screen where the result is displayed via a get-request. This also avoids the problem of retransmission of a form if the user accidentally hits the refresh button. The simplest way I have found to implement Redirect-on-POST in Spring-MVC is by overriding SimpleFormController.onSubmit:
protected ModelAndView onSubmit(Object command) throws Exception {
categoryDAO.update((Category)command);
return new ModelAndView("redirect:show.html", "id", ((Category)command).getId());
}
Subcategories and parent categories
Back in the integration test, it is about time we made the Category object a bit more interesting. Creating a hierarchy will introduce new challenges. So, then, let’s see if we can express it as a test:
public void testShowChildren() {
// Create a hierarchy of test categories
Category parentCategory = new Category("parent category " + System.currentTimeMillis());
Category subcategory = parentCategory.addSubcategory("first subcategory " + System.currentTimeMillis());
Category extraSubcategory = parentCategory.addSubcategory("extra subcategory " + System.currentTimeMillis());
categoryDao.insert(parentCategory);
categoryDao.insert(subcategory);
categoryDao.insert(extraSubcategory);
beginAt("category/index.html");
clickLinkWithText(parentCategory.getName());
// check that both subcategories are displayed
assertTablePresent("subcategories");
assertTextInTable("subcategories", subcategory.getName());
assertTextInTable("subcategories", extraSubcategory.getName());
// check that we can go to a subcategory
clickLinkWithText(subcategory.getName());
assertTextInElement("name", subcategory.getName());
}
Besides the extra test data, this is a straightforward test case. You can see that we are starting to introduce more functionality in the Category with subcategories. This means that eventually, the Category class will be relatively complex. In order to support this, now would be a good time to start writing unit tests for the Category. See the companion source code for how I’ve done this. [NB: Reader: would it be better to include this in the text?] As we bind the domain object directly to the view, the change to get the test to pass is pretty simple: We need to create a subcategories field with a getter in the Category, and use this in the “show.ftl” FreeMarker template. Here is the code for the template:
<h2>Subcategories
<table id="subcategories">
<#list category.subcategories as subcategory>
<tr><td><a href="show.html?id=${subcategory.id}">${subcategory.name}</a></td></tr>
</#list>
</table>
Similarly, it would be a good idea to have a link back to the parent. Calling “clickLinkWIthText” should be sufficient to verify that the code is working. And the corresponding FreeMarker template code: <p>Parent: <a href="show.html?id=${category.parent.id}" id="parent">${category.parent.name}</a></p>
. Now, we only want the Parent to be displayed if this isn’t a top-level category. Do we need a test for this? Maybe. But with FreeMarker, we don’t have to. Running the tests again will produce an error as we get NullPointerExceptions for ${category.parent.id}. To fix this, surround the Parent link with the following #if-directive: <#if category.parent??>
. This is a FreeMarker null-test.
Still to come
We have successfully laid the foundations for testing web applications. It’s about time to start tackling some more serious issues. Select-input with Spring-MVC is not quite as trivial or well-documented as it should be. The next thing we will explore is how to create a select-option for a many-to-one relationship. Next, I will look at implementing the DAO in Hibernate. Finally, we will look at the steps to take the code all the way into production.