Integration testing: Many-to-one relationships with select-fields
This post is an entry in my ongoing series about web integration testing with Jetty. See post 1, 2 and 3 in the series for more information.
I often find that a web interface needs to let a user select one of a list of objects as a relation to the object he is editing. This is the basic and simple way of dealing with many-to-one relationships. However, most web frameworks don’t make this as easy or well-documented as it should be. In this post, I will describe how to make a select-field with Spring-MVC and FreeMarker.
As always, I will start with a test:
public void testEditParent() {
// Create a hierarchy of test categories
Category parentCategory = new Category("parent category " + System.currentTimeMillis());
Category subcategory = parentCategory.addSubcategory("first subcategory " + System.currentTimeMillis());
Category newParent = new Category("new parent category " + System.currentTimeMillis());
categoryDao.insert(parentCategory);
categoryDao.insert(subcategory);
categoryDao.insert(newParent);
// go to the subcategory
beginAt("/category/edit.html?id=" + subcategory.getId()));
assertOptionEquals("parent", subcategory.getParent().getName());
// Update the parent field
selectOption("parent", newParent.getName());
submit();
assertTextInElement("parent", newParent.getName());
// Check that the new parent has the subcategory
beginAt("/category/edit.html?id=" + extrasubcategory.getId()));
assertTextInTable("subcategories", subcategory.getName());
// Check that the old parent no longer has the subcategory
beginAt("/category/edit.html?id=" + parentCategory.getId()));
assertTextNotInTable("subcategories", subcategory.getName());
}
Before we start: Lets do some unit testing
The test we’re working on requires quite advanced behavior from the Categories. Even though integration testing with Jetty is fast, it’s not comparable to real unit tests. To make sure that we don’t waste time testing things in the wrong place, let’s write some unit tests for the category class:
public class CategoryTest extends TestCase {
public void testUpdateParent() {
Category oldParent = new Category("old parent");
Category newParent = new Category("new parent");
Category child = oldParent.addSubcategory("child");
child.setParent(newParent);
assertEquals(newParent, child.getParent());
assertTrue("Expected to find " + child + " in " + newParent.getSubcategories(),
newParent.getSubcategories().contains(child));
assertFalse("Expected to not find " + child + " in " + oldParent.getSubcategories(),
oldParent.getSubcategories().contains(child));
}
}
Getting the test to pass requires a bit of logic in Category.setParent. See the companion source code if you’re curious about it. With the basic behavior in place, it is time to start tackling the web interface. I will ignore null parent values for now. This means that the first iteration of the views will often fail horribly if you try and click around and look at them with a web browser. However, getting these early successes allows us to have visible progress. Spring-MVC has @spring.formSingleSelect FreeMarker macro. Sadly, I have not found this to be workable when the options are a list of objects. We’re on our own. I will start with the edit.ftl view:
<p>
Parent:
<@spring.bind "category.parent" />
<select name="${spring.status.expression}">
<#list parentOptions as categoryOption>
<option value="${categoryOption.id}"
<#if spring.status.value == categoryOption>selected="true"</#if>
>${categoryOption.name}
</#list>
</select>
<@spring.showErrors "
"/>
</p>
[TODO: Better name for parentOptions?] In particular, notice that I use the categoryOption.id as the value, and the categoryOption.name as the text. In order for this to work, the name “parentOptions” must be set in the model. This is done in CategoryController. SimpleFormView has a method that is intended just for this:
public class CategoryController extends SimpleFormController {
protected Map referenceData(HttpServletRequest request) throws Exception {
Map model = new HashMap();
model.put("parentOptions", categoryDAO.listAll());
return model;
}
}
Now, the view is displayed. But the update doesn’t work correctly. We get a conversion error with Spring, as we haven’t said how we convert from the integers returned in the form to the Category parameter for setParent. Again, Spring MVC’s SimpleFormController has a method for this:
protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception {
binder.registerCustomEditor(Category.class, new PropertyEditorSupport() {
public String getAsText() {
return ((Category)getValue()).getId().toString();
}
public void setAsText(String text) throws IllegalArgumentException {
setValue(categoryDAO.get(Long.parseLong(text)) );
}
});
}
[TODO: Check this for errors, especially on POST versus GET requests. When will we see these problems?] Shazam! Updates work.
Dealing with null values
If you start the web server and start looking at the web pages, you will notice that null-values are not handled correctly. First: There is not value in the list to select if you want to set the parent property to null, and second, if the property is already null, the first item on the list is selected instead. Here is a test that illustrates what we want to have happen when dealing with null values:
public void testEditParentToNull() {
// go to the subcategory
beginAt(getEditUrl(subcategory));
// select "null" as the new parent
selectOption("parent", "");
submit();
// In show-view, the "parent" element should now be gone
assertElementNotPresent("parent");
// check that the select option has the correct value of ""
beginAt(getEditUrl(subcategory));
assertOptionEquals("parent", "");
}
The test will fail because there is no select option with the text “”. The easiest way of dealing with null values is to have an item in the list that can be used for this. There are a few different approaches - the one I have ended up using is to add an actual “null” to the end of the parentOptions:
protected Map referenceData(HttpServletRequest request) throws Exception {
Map model = new HashMap();
List parentOptions = categoryDAO.listAll();
// include null values
parentOptions.add(0, null);
model.put("parentOptions", parentOptions);
return model;
}
This requires a change in the view as well. The new code is rather ugly [TODO: Anyone knows how to make it prettier?]:
<p>
Parent:
<@spring.bind "category.parent" />
<select name="${spring.status.expression}">
<#list parentOptions as option>
<#if option??>
<option value="${option.id}"
<#if spring.status.value?? && spring.status.value == option>selected="true"</#if>
>${option.name}</option>
<#else>
<option value=""
<#if !spring.status.value??>selected="true"</#if>
>
</#if>
</#list>
</select>
<@spring.showErrors "
"/>
</p>
The first part of the test, displaying the form will now work. But on the update, we will get a NumberFormatException in the controller as we attempt to convert "" to a Long. The cure is simple enough: Make the PropertyEditor for Categories advanced enough to deal with null values and empty strings. The code is trivial, but if you’re interested, you can take a look at the companion source code.
Conclusion?
I have discussed how to display and edit many-to-one relationships with form select lists using Spring-MVC and FreeMarker. At this point in time, a question presents itself: To what extent are our tests representative of what we will see when we take the code into production? As it turns out, Hibernate has a few nice surprises in stock for us. These will be the subject of the next post in this series.
Comments:
[Wagner O .Wutzke] - Nov 8, 2007
Very Nice Post!
it has helped me a lot!
Thanks!
[Ramesh] - May 16, 2010
Fantastic. Saved me days of research. Thanks a lot for making it so simple