Refactoring – DRY Code and DRY Tests

dryrefactoringtdd

At some point I had to create some class "Class1" and that class needs a method "method". So I have the following:

Class1MethodTest: A total of N tests that check the behavior of Class1.method
Class1 method: A full implementation of the method

But a bit later I need a different class "Class2" to have a completely similar method "method". Now I have several approaches:

Approach 1

Class1MethodTest: A total of N tests
Class1 method: Full implementation
Class2MethodTest: Another set of identical tests
Class2 method: Another full implementation

Pros: Stupid simple

Cons: not DRY

At least that's the first attempt and I might even write this before doing any refactoring, right?

Approach 2:

_hidden_private_implementation_function: Full implementation of required method
Class1MethodTest: A total of N tests
Class1 method: Call hidden_private_whatever
Class2MethodTest: Another set of identical tests
Class2 method: Also call hidden_private_stuff

Pros: DRY code, still stupid simple

Cons: Tests aren't DRY, "Test interface, not implementation"

Approach 3:

MethodTest: A total of N tests
TotallyPublicCommonMethod: Full implementation of required method
Class1MethodTest: Just one test to verify that Class1 method calls the Public one
Class1 method: Call public common method
Class2MethodTest: One more test
Class2 method: Also call common method

Pros: DRY, stupid simple

Cons: .. any other than "You're testing implementation, not interface"?

Approach 4:

This is where it gets a bit exotic. Python allows me to DRY the "Approach 3" directly:

_hidden_private_implementation_function: Full implementation of required method
makeTestForClass(cls): return a total of N tests for class cls
Class1MethodTest = makeTestForClass(Class1)
Class1 method: Call hidden_private_whatever
Class2MethodTest = makeTestForClass(Class2)
Class2 method: Also call hidden_private_stuff

Pros: DRY, "Don't test implementation"

Cons: Not that simple. Too hard to modify if I ever decide to change something in Class1.method, but not Class2.method

I can think of a couple more approaches but those are not very different from these above.

Right now I have some code looking something like "Approach 1" and I am thinking on which way I should go next to make it all cleaner and better.

Best Answer

I generally write enough tests to give me confidence that my implementation is correct, but no more than that. How many tests this is depends on the problem at hand.

If you're feeling very unsure about the correct implementation of the behaviour, you'll probably end up writing a full set of tests for each endpoint and refactoring at the end: going from Approach 1 to Approach 2, as you've labelled them. Kent Beck calls this sort of programming "driving in a low gear".

If your problem is not so complex, you might want to change up a gear or two. Remember, TDD is about doing "the simplest possible thing" at every step. If you can see that the simplest way to make your next test pass is just to call the method you've already implemented, then you can go home early.

If you get an unexpected bug or test failure, you're free to slow down, write more tests, and drive in a lower gear again. TDD often feels like this: constantly adjusting your pace to match your confidence and fear levels. It's one of the themes of Kent Beck's book.

Remember, you're not aiming for 100% coverage of every public method; just enough to give you confidence that your code is correct.

Related Topic