An Obsession with Everything Else

http://www.derrickschneider.com/atom.xml

Wednesday, December 31, 2008

Unit Testing The UI

The conventional wisdom on unit tests is that they stop at the UI. There are, after all, QA-centric tools that automate the tasks of pressing buttons, choosing menu items, and putting text into forms.

But a lot of what I do at work is writing code that generates web pages. It's been relatively easy to introduce unit tests into utility classes or the service layer, but I wanted to give myself the same stability in the UI universe. Since my team is good about separating models, views, and controllers, I figured out an easy way to bring unit tests up against the UI: Execute methods in the controller, and then inspect the resulting model.

We use the Spring framework, which makes this concept pretty easy. It even includes mock objects for setting up HTTP requests and responses. The generic Spring flow enforces good separation: Your controller's handle method gets called and is expected to return a ModelAndView object. Your unit test can call the same method with appropriately configured mock request objects and then peer into the model object to make sure that values are set properly.

Since our views are usually JSPs, which require a running web container, I don't think there's a good way to write unit tests for that layer. But since the view relies on the model being correct, and the unit tests can look at the model, I think it's a pretty good stopping point. I don't want to rewrite those automated button-pushing tools, after all.

Trying to incorporate this idea into my Mac/iPhone programming, I realized I could go a little bit further than I could in the web container world. I'm working on an iPhone app to enable crazy Derrick meal planning, and one of the controllers is responsible for showing me things I have to do on any one day. I started with that as an experiment. To enable my "UI-ish" unit tests, I have split Apple's default template for this controller into a definite Model object and a definite "ViewLogic" object. The ViewLogic object handles code for formatting the date, determining if the table is present, how many sections it has, and what the title of each section is. All of these vary depending on the state of the model, because there are different categories of things to worry about for any given day, and you could have 0-3 of those categories. My test invokes methods on my controller that update the model object. Then I inspect that model and execute various methods on the ViewLogic object to make sure they're returning correct values based on the model.

Now I can refactor the methods in ViewLogic, some of which have a relatively high cyclomatic complexity, with a high confidence that I won't break the code that relies on it or introduce new bugs.

There are a couple of consequences to this, though. One is a bit of class bloat. I can re-use the model object for at least one other view, but the ViewLogic object is tailored to that one view. The other consequence is that the ViewLogic has become an adapter, converting the methods that the framework expects to find (which have context about views and such that don't exist in the unit test world) into methods I define. Rather than doing the normal thing, then:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// do calculations about number of rows
}


I do this:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self rowsInSection: section];
}


Not necessarily bad, but it does make those methods a little anemic. I could pass nil as the table view, but I could see my code at some point needing to execute a method on the table view. So better to isolate "number of rows" logic in a method where I can call it even if I don't have a runtime UITableView object.

0 Comments:

Post a Comment

<< Home