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.

6 comments:

S. Potter said...

I like and use jMock on the legacy Java apps I maintain (yes, I am in Ruby land the majority of the time now, where my services are only limited by hardware NOT the language they are written in rather than both as in the Java world).

As far as Java APIs go I really enjoy using jMock (in contrast to the vast majority of Java APIs, frameworks and/or SPIs I am forced to use on my projects).

One thing that is getting under my nails right now though in jMock is how to stub a constructor (e.g. FileInputStream's) to throw an exception. It seems this should be obvious, but I have yet to figure it out this evening.

Thoughts, ideas or brainwaves are most welcome.

To provide feedback please visit my blog where I plan on writing about my Java vs. Ruby BDD adventures at:
http://snakesgemscoffee.blogspot.com

Thanks!

Colin Jack said...

This is a very interesting post but I think there is more to the original point.

Regardless of how you do it testability can influence designs in ways that are not necessarily positive. Encapsulation and simplicity sometimes fight against the need to mock/replace/get at everything in the design. If you want to test at the most granular level using interaction testing, or want to use IOC for nearly every little bit of the design, then you are going to increase complexity and in some cases I don't think it does gather any benefits at all.

Steve Freeman said...

Of course there's a lower limit to when to use interaction testing (we even have a test-smell for that :), but I would guess that the greatest problems these days are in the other direction, code that has too many assumptions built in.

One of the worst cases I came across was a .Net framework I was supposed to use that required a local installation of ActiveDirectory and a running ActiveDirectory service somewhere so I wouldn't accidentally break the values in production. The intention was that it would "just work", but in practice I never got that far.

Anonymous said...

Yeah you are no doubt right.

I think your going to get it from both sides though. Those who don't get decoupling will complain about the very idea of not using the real objects, but equally if your advice leads to complex tests or designs then you'll get attacked for that. Oh well :)

Whilst I've got the chance can I ask what you mean with this statement in the mock roles paper:

"We continue this process until we reach a layer that implements
real functionality in terms of the system runtime or external
libraries."

Steve Freeman said...

It means we work our way from outside in, until we have to deal with something outside our code, such as part of the runtime or a third-party framwork.

Steve Freeman said...

It means we work our way from outside in, until we have to deal with something outside our code, such as part of the runtime or a third-party framework.