In my previous post I talked about how unit tests aren’t born tests, but are a design aid which turn into tests as the components they shape mature. But testing doesn’t stop at driving out good, decoupled objects. Good developers take both broad and detailed views of system testing, which normally involves writing functional tests to validate scenarios.
But let’s start smaller. Given we want to use unit tests to drive out the design of all layers of a system – including those layers which are exposed to users or other systems – there’s an chance that we may end up writing code in unit tests that ends up being similar in nature to parts of our functional tests. And if so the argument goes our code may not be DRY, and the team may not be “agile” and Mr Fowler may be upset once more.
In fact this is a rather poor argument. Once you accept that unit tests are not tests but a design device, that any resemblance between unit test code and functional test code is accidental, and is likely to be transitory as system design changes through delivery, you understand that these two kinds of “tests” are inherently different beasts.
To help clarify this, I’d like to use an example from the world of Web-based (though not necessarily RESTful) services. In our book, Savas, Ian and I use a coffee shop called Restbucks to exemplify various techniques and often we capture those techniques as code snippets to help ground them in practical reality. In our chapter on CRUD services one of the implementation options uses a Java servlet to provide a means of placing and manipulating coffee orders via the network.
In designing the service, we need to isolate specific components and model their design and interaction with collaborators. We certainly didn’t want to have to run the servlet in a real container at design time because that slows down development and doesn’t help with formulating good designs. Instead where we needed to design how the servlet would interact with its collaborators, we used mock HttpServletRequest and HttpServletResponse objects (specifically the MockHttpServletRequest and MockHttpServletResponse from Spring) to drive out a set of behavioural contracts between the servlet and its consumers for given test contexts. By following TDD-ish techniques, we drove out the design of the service’s constituent components, incrementally adding functionality until we could honour the behavioural contracts placed on us by our unit tests.
Our QA department – the same three authors with day jobs! – couldn’t have cared less. “They” couldn’t care how we designed and implemented the servlet, only that the overall service matched their expectations from a functional point of view, from the point of view of the consumer of the service. “They” didn’t try to re-use our test code in some relentless pursuit of DRY-ness because functional testing does not (and should not) test component and interaction design, it tests functionality. If you look hard, there’s a even a clue in the name!
We did in fact write functional tests which exercised the servlet and which occasionally had passing resemblance to some of our unit tests, especially those tests which deal with the payload of HTTP requests and responses But those functional tests maintain *functionality* throughout the development where the unit tests protect good design. The different is that functionality needs to be unswervingly preserved in hi-fidelity into production whereas design often changes, driven by unit tests of course.
In a dependable system, we need design time contracts expressed as unit tests, much as we need functional tests to guarantee the presence of expected, correct functionality. But these are different horses for different courses. Functional tests can be used to show when a system is ready, but they are no real indicator of the quality of the system, of its ability to be chained, maintained, and updated since that is a function of good design. Similarly unit tests can’t be used to show when a system has met its users’ requirements, since that it is a matter of functionality. But taken together functional and unit testing – holistically – can be used to demonstrate functionality and design.
And that is goodness.