views:

239

answers:

6

I am playing with a toy project at home to better understand Test Driven Design. At first things seemed to be going well and I got into the swing of failing tests, code, passing test.

I then came to add a test and realised it would be difficult with my current structure and that furthermore I should split a particular class which had too many responsibilities. Adding even more responsibilities for the next test was clearly wrong. I decided to put aside this test, and refactor what I had. This is where things started to go wrong.

It was difficult to refactor without breaking lots of tests at once, and then the only option seemed to be to make many changes and hope I ended up back at something where the tests passed again. The tests themselves were valid, I just had to break nearly all of them while refactoring. The refactoring (which I'm still not that happy with) took me five or six hours before I had returned to all tests passing. The tests did help me along the way.

It feels like I got off the TDD track. What do you think I did wrong?

As this is mostly a learning exercise I'm considering rolling back all that refactoring and trying to move forward again in a better fashion.

A: 

Perhaps you were testing a too low a level. It's hard to say without seeing you code, but typically I test a feature from end to end, and make sure all the behaviour I expected to happen, happened. Testing every single method in isolation will give you the test-web you have created.

You can use tools like NCover and DotCover to check that you haven't missed any code paths.

Andrew Bullock
My tests were generally: construct some part of the domain model, give it an event or two from the (simulated) user interface and assert what the result is by querying the domain model. My tests were using the same interface that the UI layer does because I built it top-down. (Some other tests were just unit tests on one particular class to get that right).
WW
Unit Testing is testing every single method in isolation. TDD is creating unit tests before coding the implementation.
btlog
The tests I called "unit tests" were still written first. They were just focused on a single class. The others had a larger scope (starting with a simulated GUI event and through to a simulated GUI outcome).
WW
+8  A: 

Perhaps you went too fast when splitting your class. The steps for the Extract Class Refactoring are as follows:

  • create the new class
  • have an instance of that class as a private data member
  • move field to the new class, one by one
  • compile and test for each field
  • move method to the new class, one by one, testing for each

That way you won't break a large number of tests while refactoring your class, and you can rely on the tests to make sure nothing was broken so far all along the class splitting.

Also, make sure you're testing the behavior, not the implementation.

philippe
Carl Manaster
Thanks for this answer, I may revist the code and try this. It's hard to explain without posting lots of code, but the responsibilities I'm trying to separate are "what the user wants to happen" and "what is allowed to happen". These concepts are mingled in the class I've realised.
WW
Don't forget to use dependency injection for the extracted class into the old class
Gutzofter
I rolled back everything and started moving forward from the last point I thought I was still on track. I've refactored (assisted by Eclipse) and did not go more than a few minutes without all tests green. This second attempt has resulted in a completely different refactoring to my first (failed) attempt. Thanks for the help.
WW
Thanks for the feedback, WW.
philippe
A: 

The only thing "wrong" was to add a test afterwards. In "true" TTD you first declare all tests before the actual implementation. I say "true" because that's often only theory. But in practice you still have the safty given by the tests.

Mene
I think he meant that the test he added after doing some work was for a new feature, not for existing code.
Tomas Lycken
I was going to add a test for the next piece of functionality when I realised it would not fit without further bloating a existing class. That's when I decided I needed to refactor before attempting to add more features (updated the question to hopefully clarify).
WW
Ah ok... yeah, designing a system in a way that it's extensible for unforeseen features is always a big challenge. I guess that's one of those thing where reading books won't get you too far and (mostly) everything comes down to experience.
Mene
A: 

This is, sadly, something TDD proponents don't talk about enough and that makes people try TDD and then abandon it.

What I do is what I call "High Level Testing" which consists in avoiding unit tests and doing exclusively high level tests (which we could could call "integration tests"). It works pretty well and I avoid the (very important) problem you mentioned. I wrote an article about it a while ago:

http://www.hardcoded.net/articles/high-level-testing.htm

Good luck with TDD, don't give up yet.

Virgil Dupras
-1, because unit tests are very important, very valuable. High level tests tell you something has gone wrong - and that's very valuable, too - but unit tests tell you what has gone wrong. Don't do without them.
Carl Manaster
But then what happens is that unit tests become a liability because they make refactoring more time consuming. When that happens, you become *less prone* to refactoring. That's bad.
Virgil Dupras
Unit test only become a liability if you let them. Refactoring actually is something I do with a lot more confidence with plenty of unit tests around. Yes, you may have breaking tests for a while, but this can be avoided in the way phillippe describes, or is something you just take for granted while working on the refactoring and seeing the tests fall back into "green" one by one and sometimes many by many.
Marjan Venema
I'm not sure I made myself understood here. With high level tests, you get the same coverage as with unit tests, so refactoring is still made with "safety nets". It's just that when you split/merge classes, you don't have to modify your tests (which you have to when you have unit tests). Modifying your tests when doing refactoring is something error-prone that should be avoided.
Virgil Dupras
All: given this is a toy project only a few hours old, there is not that much difference between testing everything and testing a given class. Almost all my tests, however, are at the upper most level.
WW
+2  A: 

I wanted to comment the accepted answer but my current reputation does not allow me. So here it is as a new answer.

TDD says:

Create a test that fails. Code a little. Make the test pass.

It insists on coding in tiny steps (especially when beginning). View TDD as systematic validation of successive refactorings you perform to build your programs. If you take too big a step, your refactoring will get out of control.

Philippe A.
But I was in a position where all my tests were passing (except for the last one I had just written), but I was unhappy with the structure of the code as it did not easily allow me to implement the next test. After this, the refactoring I performed went bad.
WW
A: 

Also for TDD continuos regression of test cases are also necessary. So Continuos integration with Coverage tools(as mentioned above) are necessary. So that small changes(i.e Refactoring) can be regressed easily and whether any code path which are missed can be found easily.

Also I feel that if tests were not written previously, time should not be wasted to think whether to write tests or not. Tests should be written immediately.

dhinesh