views:

162

answers:

4

In the article Test for Required Behavior, not Incidental Behavior, Kevlin Henney advises us that:

"[...] a common pitfall in testing is to hardwire tests to the specifics of an implementation, where those specifics are incidental and have no bearing on the desired functionality."

However, when using TDD, I often end up writing tests for incidental behaviour. What do I do with these tests? Throwing them away seems wrong, but the advice in the article is that these tests can reduce agility.

What about separating them into a separate test suite? That sounds like a start, but seems impractical intuitively. Does anyone do this?

A: 

The problem you describe is very real and very easy to encounter when TDD'ing. In general you can say that it isn't testing incidental behavior itself which is a problem, but rather if tons of tests depend on that incidental behavior.

The DRY principle applies to test code as well as to production code. That can often be a good guideline when writing test code. The goal should be that all the 'incidental' behavior you specify along the way is isolated so that only a few tests out of the entire test suite use them. In that way, if you need to refactor that behavior, you only need to modify a few tests instead of a large fraction of the entire test suite.

This is best achieved by copious use of interfaces or abstract classes as collaborators, because this means that you get low class coupling.

Here's an example of what I mean. Assume that you have some kind of MVC implementation where a Controller should return a View. Assume that we have a method like this on a BookController:

public View DisplayBookDetails(int bookId)

The implementation should use an injected IBookRepository to get the book from the database and then convert that to a View of that book. You could write a lot of tests to cover all aspects of the DisplayBookDetails method, but you could also do something else:

Define an additional IBookMapper interface and inject that into the BookController in addition to the IBookRepository. The implementation of the method could then be something like this:

public View DisplayBookDetails(int bookId)
{
    return this.mapper.Map(this.repository.GetBook(bookId);
}

Obviously this is a too simplistic example, but the point is that now you can write one set of tests for your real IBookMapper implementation, which means that when you test the DisplayBookDetails method, you can just use a Stub (best generated by a dynamic mock framework) to implement the mapping, instead of trying to define a brittle and complex relationship between a Book Domain object and how it is mapped.

The use of an IBookMaper is definitely an incidental implementation detail, but if you use a SUT Factory or better yet an auto-mocking container, the definition of that incidental behavior is isolated which means that you if later on you decide to refactor the implementation, you can do that by only changing the test code in a few places.

Mark Seemann
A: 

"What about separating them into a separate test suite?"

What would you do with that separate suite?

Here's the typical use case.

  1. You wrote some tests which test implementation details they shouldn't have tested.

  2. You factor those tests out of the main suite into a separate suite.

  3. Someone changes the implementation.

  4. Your implementation suite now fails (as it should).

What now?

  • Fix the implementation tests? I think not. The point was to not test an implementation because it leads to way to much maintenance work.

  • Have tests that can fail, but the overall unittest run is still considered good? If the tests fail, but the failure doesn't matter, what does that even mean? [Read this question for an example: http://stackoverflow.com/questions/1406552/non-critical-unittest-failures%5D An ignored or irrelevant test is just costly.

You have to discard them.

Save yourself some time and aggravation by discarding them now, not when they fail.

S.Lott
+1  A: 

In my experience implementation-dependent tests are brittle and will fail massively at the very first refactoring. What I try to do is focus on deriving a proper interface for a class while writing the tests, effectively avoiding such implementation details in the interface. This not only solves the brittle tests, but it also promotes cleaner design.

This still allows for extra tests that check for the risky parts of my selected implementation, but only as extra protection to a good coverage of the "normal" interface of my class.

For me the big paradigma shift came when I started writing tests before even thinking about the implementation. My initial surprise was that it became much easier to generate "extreme" test cases. Then I recognized the improved interface in turn helped shape the implementation behind it. The result is that my code nowadays doesn't do much more than the interface exposes, effectively reducing the need for most "implementation" tests.

During refactoring of the internals of a class, all tests will hold. Only in cases where the exposed interface changes, the test set may need to be extended or modified.

Timo
Hi Timo. Thanks for your answer. The difficulty is, with TDD, the tests drive the implementation and therefore are by definition implementation dependent. That's OK as far as I know my implementation is 100% test covered, and I can freely refactor the implementation (using Feathers' definition of refactoring - i.e. behaviour doesn't change). The trouble I'm having is with the "don't test for incidental behaviour" philosophy: it aesthetically makes sense to me but practically I don't see how I can do it *without* losing the implementation coverage my TDD tests provide.
Matt Curtis
(I now added my personal test-first insights to my answer.)
Timo
A: 

I you really do TDD the problem is not so big as it may seem at once because you are writing tests before code. You should not even think about any possible implementation before writing test.

Such problems of testing incidental behavior is much more common when you write tests after implementation code. Then the easy way is just checking that the function output is OK and does what you want, then writing test using that output. Really that's cheating, not TDD, and the cost of cheating is tests that will break if implementation change.

The good thing is that such tests will break yet more easily than good tests (good test meaning here tests depending only of the wanted feature, not implementation dependent). Having tests so generic they never break is quite worse.

Where I work what we do is simply fix such tests when we stumble upon them. How we fix them depends on the kind of incidental test performed.

  • the most common such test is probably the case where testing results occurs in some definite order overlooking this order is really not guaranteed. The easy fix is simple enough: sort both result and expected result. For more complex structures use some comparator that ignore that kind of differences.

  • every so often we test innermost function, while it's some outer most function that perform the feature. That's bad because refactoring away the innermost function becomes difficult. The solution is to write another test covering the same feature range at outermost function level, then remove the old test, and only then we can refactor the code.

  • when such test break and we see an easy way to make them implementation independant we do it. Yet, if it's not easy we may choose to fix them to still be implementation dependant but depending on the new implementation. Tests will break again at the next implementation change, but it's not necessarily a big deal. If it's a big deal then definitely throw away that test and find another one to cover that feature, or change the code to make it easier to test.

  • another bad case is when we have written tests using some Mocked object (used as stub) and then the mocked object behavior change (API Change). This one is bad because it does not break code when it should because changing the mocked object behavior won't change the Mock mimicking it. The fix here is to use the real object instead of the mock if possible, or fix the Mock for new behavior. In that case both the Mock behavior and the real object behavior are both incidental, but we believe tests that does not fail when they should are a bigger problem than tests breaking when they shouldn't. (Admitedly such cases can also be taken care of at integration tests level).

kriss
Hi kriss. Thanks for your answer. We are doing TDD in the true sense, and the implementation is completely driven by tests (following Uncle Bob's minimalist rules, writing just enough of a test etc. etc.), but that means the tests *by definition* are implementation specific, right? I think that might be OK in some sense, as long as the tests test small enough components, and the components are separable. But how to organise the tests for behaviour, vs implementation? And is there overlap between (bad) incidental tests and (worthwhile for behaviour-preserving refactoring) implementation tests?
Matt Curtis