Unit Testing – testing class contracts


It’s been a while since I last posted, and I thought I’d write something that often gets overlooked when unit testing – what the boundary is for a given set of unit tests.

When you write unit tests on e.g. a public method on a class, something that you need to keep in mind is that you’re testing the external behaviour of that method – not the internal implementation of it. What is external behaviour? We can measure this in several ways: –

  • The return value of a method. Does it give the expected result given a set of inputs or state?
  • Calls to other classes. Does it call methods on its dependencies with the correct arguments under the correct circumstances?
  • Mutation of state on the class. Does it mutate its public state after the call?
  • Mutation of state on another class. Does it mutate the state of another class after the call?

I want to address the second one here – calling other classes. Normally you would do this with a mocking framework. You might call a dependency via an interface, which is mocked out, and then check whether it was called with the correct arguments etc. Look at the following three arbitrary examples: –

image

image

image

All three do exactly the same thing – they call the data access layer with a value.

  • The first one does it directly.
  • The second one does the bulk of the work via a couple of private methods.
  • The last one does it via a helper class, Service Layer.

You would be surprised at the amount of times people suddenly change their testing strategy for these three examples, by either making otherwise private fields public or internal or such, simply to “prove” that e.g. the public method called a private method.

To me, there’s very little difference between them all. The first two examples are basically identical. The fact that you have a private method performing some of the logic is irrelevant – remember, we’re testing the behaviour of the class, and not the internal implementation of it. To the external caller, and our unit tests, the behaviour is the same.

The third example is an interesting one – if you have a more powerful mocking framework like Typemock, you can mock out calls to the Service Layer class and prove the contract between MyClass and ServiceLayer. On the other hand, the majority of us use mocking frameworks such as Rhino Mocks or Moq, which only allow mocking of interfaces or virtual methods. So, again, to my mind, the behaviour is still the same. The fact that it happens to call another class that you’ve made can again be considered irrelevant – the real test is that when you call Foo with a number, you call IDataAccessLayer with number + 10.

Conclusion

If you find that testing the behaviour of your class is too onerous e.g. your setup grows too large, consider this a code smell that unit testing gives you for free that you are violating SRP. Fix it by splitting the class into several others and test the contract for communication between them via stubbed interfaces etc..

When writing your unit tests, think about what you consider to be the “contract” of that class or method. How should it behave? Where does its responsibility end? Normally this will be handing over to another class that’s typically stubbed out – this is not necessarily the same as saying “just test the code that lives within the method” – if you do not mock out methods that your public entry method is dependent on, you must test those methods out fully as well. So if public API method A calls private method B which calls private method C, as far as the behaviour of method A is concerned, you should test all logical paths of all three methods.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s