Saturday, May 7, 2016

Examples of tiny test-induced design damage

Imagine you are trying to write a unit test for some code, but you're finding it difficult.

Maybe there's some complex detail in the middle of a method that is not relevant to the current test, and wouldn't it be nice to disable that bit of code just for the purpose of the test? Maybe you could add an optional boolean parameter to the method, which when set causes the detail to be skipped.

With the exception of getting legacy code under test to support you when refactoring, I see this as a bad thing, making the code worse just for the sake of testing.

Here's my list so far:
  • method marked 'internal' for testing
  • method marked 'virtual' for testing
  • method overload for testing
  • additional optional method parameter, only used for testing
  • public field that is only modified under test, to change behavior for testing
  • public field that is only read by tests
  • function replaced with mutable delegate field, only mutated for testing
Yes, TDD is about letting tests influence your design, but not in this way!

So how do you tell the difference? Here are a few ways:

  • Will this be used for both testing and in production? 
  • Do you feel the urge to add a comment saying why you did this?
  • If you removed the tests, would you keep this design?
  • Your own design sense. Do you think the design is better?
What to do about it?

Usually the desire to do this indicates that your class/function/module whatever is doing too much. 

Maybe you need to extract a class. If it's not obvious what belongs in the class, you might need to extract some methods first, to put in that new class.

A really common case is primitive obsession, like if the method deals with some string. If you move the string in to a new class, and then move that "deals with the string" code in to the class, then the class is small and easy to test and your code has improved. This is Whole Value.

Maybe there's something at the beginning or end of the method that talks to an external system, and that is making testing difficult. You could move those lines to the caller, and the method becomes testable.

I'd like to find some concrete examples.

No comments: