Wednesday, June 07, 2006

More Test Driven Development observations

In part one I described some of the observations I've made while following Test Driven Development practices. In part two I'll describe other observations that I have previously lightly touched on or haven't documented at all.

If your object graph will not permit testing each concrete class individually, your object graph requires refactoring. Look to introduce Dependency Injection to resolve the issue.

ObjectMother is an artifact, a legacy testing pattern. In the early days of TDD, simply writing tests was a large step. However, testing has evolved to separated unit and functional tests. Unit tests should test individual classes; therefore, ObjectMother is clearly not an option. Functional tests should use actual implementations so ObjectMother could be a potential fit. However, unit tests should test boundary cases and functional tests should only test the classes interactions. Therefore, there should be a minimal amount of functional tests. Since the number of functional tests should remain low, there should not be enough duplication to warrant use of ObjectMother. FluentInterface (or the special case of FluentInterface, InitializationChain) is often a superior alternative to ObjectMother.

In an object oriented system, classes collaborate to achieve a larger goal. However, tests are not object oriented. In fact, having tests depend on other tests is an anti-pattern. Tests are procedural in nature and should be treated as such. Each test method should encapsulate the entire behavioral test. Tests should have very specific goals which can easily be identified by looking only at the method body, private method calls, and stubs if applicable.

Using patterns, for example Template Method, in tests does reduce duplication but at the expense of readability. Due to the loss in readability, setup is considered harmful. The superior solution is to create private methods that are executed when called.

Removing duplication within a system reduces the complexity of the entire system. However, the entire scope of a test is the single method body. Therefore, abstracting duplication should not be taken lightly. I rarely find that my tests contain duplication. When my tests do contain duplication, the solution is often refactoring a poorly designed piece of the object graph, not extracting a private method. For example, duplication can occur in setting up an array for testing. But, the actual problem is the getter returning an array, not the duplicate test code. When duplicate code is extracted to a private method, be aware that you are coupling two entities that in general should be decoupled. This is acceptable, but only for a minimal amount of special cases. In fact, even when a special case occurs, it is preferable to leave the duplication as is if it increases readability

Supporting classes, stubs for example, should be included as private subclasses of the test class. This ensures that to read and understand an entire test you only need to look at one file. It additionally ensures that the stub will only be used within the context of the test class. This is very valuable in keeping the tests robust.

'Magic' introduced in a test (such as using the test class as a stub) may appear clever; however, it reduces readability and should be immediately refactored to something more maintainable. Save the effort and write the test clearly in the first place.

Always use strings or numbers as the 'expected' value of an assertion instead of constants or calculated values. Nothing is more meaningless than seeing a test that asserts expected_error_message == customer.save. The major problem here is the question of which is wrong, expected_error_message or the result of customer.save. Additionally, the maintainability suffers because the values of both arguments must be obtained when debugging.

Use assert_equal (or Assert.AreEqual, etc) the majority of the time. Asserting equality results in highly readable test failures. If I see a failure similar to <"select * from customers"> expected but was <"select from customers"> I may know what the issue is without rereading the test. While the others such as assert, assert_not_null, etc have their place, the error messages are not near as meaningful. <false> is not true is very clear, but I have no idea what was false. Finding the true issue requires consulting the test.

I believe that is a fairly comprehensive list my major observations. Look for follow-ups in the future as I continue to learn more lessons.

1 comment:

  1. Anonymous2:37 PM

    You should use a mock when testing interaction even if you have already coded itineraryday. All that you need for the test is the itineraryday interface, which is what the itinerary class should depend on.

    ReplyDelete

Note: Only a member of this blog may post a comment.