01 October 2007
How to talk Mock
Brian Marick has posted an interesting proposal for changing the order of the pieces within a unit test. For example, he suggests moving from
def test_checker_normally_checks_network_once checker = testable_checker # creates checker using mocks. @network.should_receive(:ok?).once.with(”url”).and_return(true) checker.check(”url”) end
to
def test_checker_normally_checks_network_once
checker = testable_checker
during {
checker.check(”url”)
}.behold! {
@network.should_receive(:ok?).once.
with(”url”).and_return(true)
}
end
which shows what you can do if you have a language with useful constructs.
Personally, I've tended to think of stubs and expectations in terms of pre-conditions and post-conditions, so I find the pre-test setup less disturbing—and I've been doing it so long I don't see it any more. I imagine it's rather like getting used to (+ 1 2), which Brian ought to recognise. If I were to have a go, I think I might try to write some like this:
during {
checker.check("url")
}.expect_to_see {
@network.ok?("url") { returning true }
@timesink.sleep_for(32.minutes) { 6.times }
but_not @listener.inform
}
It's nice to see someone experimenting with the ideas.
Labels: design, domain-specific-language
04 July 2007
What does "easy" really mean?
Steve's recent post about the perceived conflict between Testability and Design included this quote from a user of TypeMock:
The key benefit we get from TypeMock is having the ability to fully unit-test the code without impacting the API design. [...] For us, the API is part of the deliverable. We need to make it fairly easy to consume and can't have the architecture of the solution overshadow the usability of the API.
The crux of the issue is in the words "easy to consume". What does that mean? Easy to learn? Or easy to adapt to new, unanticipated situations.
For example, many developers find the java.io API complicated. This is how to open a file for reading:
Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream("input.txt")));
The equivalents C or Python are much shorter, in Python:
reader = open("input.txt")
The Java version does, however, have a point. Is use of the Decorator and Chain of Responsibility patterns makes it easy to apply in different situations and adapt different underlying transports to the java.io stream model. In the C approach. different implementations are buried in the runtime, so you have to go to a different mechanism to try anything new.
TDD with mock objects drives an object-oriented design towards one like the java.io API. The design process focuses on discovering common patterns of communication between objects. The end-to-end system behaviour is defined by composing objects instead of writing algorithmic code. That makes code more malleable by experienced programmers but, arguably, makes it harder to learn for newcomers to the codebase or to object-oriented programming itself.
The problem can be addressed by layering expressive APIs that support common operations above the flexible, object-oriented core. A simple API is easy to learn but allows the programmer to drill down to the flexible core when new unexpected situations arise.
JMock itself follows this model. The core is an object-oriented framework for representing and checking expectations. This framework is flexible and extensible. You can create and compose objects to represent and test all sorts of expectations. This level of code, however, is too fine-grained to express the intent of a test. It's like trying to figure out what's for dinner from reading the recipes. That's why we also wrote a high-level API that is closer to the programmer's problem domain. It makes it easy for us to write readable code to set up framework objects and we still have the extension points we need when we need a new feature — and we can test jMock without manipulating bytecodes.
Labels: design, domain-specific-language, testability
21 May 2007
Apache Camel
Labels: domain-specific-language, news
19 April 2007
Test Smell: Logging is also a feature
I have a more contentious example of working with objects that are hard to replace: logging. Take a look at these two lines of code:
log.error("Lost touch with Reality after " + timeout + "seconds);
log.trace("Distance travelled in the wilderness: " + distance);
These are two separate features that happen to share an implementation. Let me explain.
Support logging (errors and info) is part of the user interface of the application. These messages are intended to be tracked by support staff, perhaps system administrators and operators, to diagnose a failure or monitor the progress of the running system.
Diagnostic logging (debug and trace), is infrastructure for programmers. These messages should not be turned on in production because they're intended to help the programmers understand what's going on whilst they're developing the system.
Given this distinction, I should consider using different techniques to develop these two type of logging. Support logging should be Test-Driven from someone's requirements, such as auditing or responsiveness to failure. The tests will make sure that I've thought about what each message is for and that it works. The tests will also protect me from breaking any tools and scripts that other people write to analyse these log messages. Diagnostic logging, on the other hand, is driven by the programmers' need for fine-grained tracking of what's happening in the system. It's scaffolding so it probably doesn't need to be Test-Driven and the messages might not need to be as consistent as those for support logs. These messages are not to be used in production, right?
Support, not logging
To get back to the point of the series, writing unit tests against static global objects, including loggers, is noisy. Either I have to read from the file system or I have to manage an extra testing Appender. I have to remember to clean up afterwards so that tests don't interfere with each other, and set the right level on the right Logger. The noise in the test reminds me that my code is working at two levels: my domain and the logging infrastructure. Here's a common example of code with logging:
Location location = tracker.getCurrentLocation();
for (Filter filter : filters) {
filter.selectFor(location);
if (logger.isInfoEnabled()) {
logger.info("Filter " + filter.getName() + ", " + filter.getDate()
+ " selected for " + location.getName()
+ ", is current: " + tracker.isCurrent(location));
}
}
Notice the shift in vocabulary and style between the functional part of the loop and the logging part (in italics)? At a micro level, the code is doing two things at once, something to do with locations and rendering support information, which breaks the Single Responsibility Principle. Maybe I could do this instead:
Location location = tracker.getCurrentLocation();
for (Filter filter : filters) {
filter.selectFor(location);
support.notifyFiltering(tracker, location, filter);
}
where the support object might be implemented by a logger, a message bus, pop-up windows, or whatever's appropriate; the detail is not relevant to the code at this level. This code is also easier to test. We, rather than the logging framework, own the support object so we can pass in a mock implementation at our convenience, perhaps in the constructor, and keep it local to the test case. The other simplification is that now we're testing for objects rather than the formatted contents of a string. Of course, we will still need to write an implementation of support and some focussed integration tests to go with it; Jeff Langr's Agile Java is one source of advice on how to do that.
But that's crazy talk…
The idea of encapsulating support reporting sounds like over-design, but it's worth thinking about for a moment. It means I'm writing code in terms of my intent (helping the support people) rather than the implementation (logging), so it's more expressive. All the support reporting is handled in a few known places, so it's easier to be consistent about how things are reported and to encourage reuse. It can also help me structure and control my reporting in terms of the application domain, rather than in terms of java packages. Finally, the act of writing a test for each report helps me avoid the "I don't know what to do with this exception, so I'll log it and carry on" syndrome, which leads to log-bloat and production failures because I haven't handled obscure error conditions.
One objection is "I can't pass in a logger for testing because I've got logging all over my domain objects. I'd have to pass one around everywhere". I think this is a test smell that is telling me that I haven't clarified my design enough. Perhaps some of my support logging should really be diagnostic logging, or I'm logging more than I need to because I left some in that I wrote when I hadn't yet understood the behaviour. Most likely, there's still too much duplication in my domain code and I haven't yet found the "choke points" where most of the production logging should go.
So what about diagnostic logging? Is it disposable scaffolding that should be taken down once the job is done, or essential infrastructure that should be tested and maintained? That depends on the system, but once I've made the distinction I have more freedom to think about using different techniques for support and diagnostic logging. I might even decide that in-line code is the wrong technique to implement diagnostic logging because it interferes with the readability of the production code that matters. Perhaps I could weave in some Aspects instead (since that's the canonical example of their use). Perhaps not, but at least I now have a choice.
One final data point. A colleague of mine once worked on a system where so much pointless content was written to the logs that they had to rolled off the disks after a week. This made maintenance very difficult as the relevant logs had usually gone by the time a bug was assigned to be fixed. If they'd not logged at all they could have made the system go faster with no loss of useful information.
Labels: design, domain-specific-language, listening to the tests, testability
26 December 2006
Mock Objects is a Technique, Not a Technology
George Malamidis recently proposed an alternative to mock objects in Groovy. Instead of using mock objects you can just use Groovy's Expando class to make an object that behaves like another type and checks that it is being invoked as expected.
But... that is a mock object!
Mock objects are not a specific technology, such as jMock or EasyMock they are a technique for the test-driven development of object-oriented code that follows a Tell, Don't Ask design style.
You can write mock objects with a framework, by using the reflection and metaprogramming facilities of a dynamic language, by coding everything by hand, or by generating the code at compile time. Whatever floats your boat.
However, faking out the mocked type is only a small part of what a mock object framework like jMock does. In fact, of the 2487 lines of code that make up jMock 2 only 34 are required to perform what we whimsically call "imposterisation". The remainder provide syntactic sugar to ensure the test code clearly expresses the intent of the test, let the programmer precisely specify the expected interactions between objects and generate clear diagnostics when tests fail. This functionality is vital if tests are to be helpful during the ongoing development of the code.
Labels: domain-specific-language, explanation, jmock
© The authors
