tags:

views:

853

answers:

5

Having recently discovered this method of development, I'm finding it a rather nice methodology. So, for my first project, I have a small DLL's worth of code (in C#.NET, for what it's worth), and I want to make a set of tests for this code, but I am a bit lost as to how and where to start.

I'm using NUnit, and VS 2008, any tips on what sort of classes to start with, what to write tests for, and any tips on generally how to go about moving code across to test based development would be greatly appreciated.

+30  A: 

See the book Working Effectively with Legacy Code by Michael Feathers.

In summary, it's a lot of work to refactor existing code into testable and tested code; Sometimes it's too much work to be practical. It depends on how large the codebase is, and how much the various classes and functions depend upon each other.

Refactoring without tests will introduce changes in behaviour (i.e. bugs). And purists will say it's not really refactoring because of the lack of tests to check that the behaviour doesn't change.

Rather than adding test across the board to your whole application at once, add tests when you work in an area of code. Most likely you'll have to return to these "hotspots" again.

Add tests from the bottom up: test little, independent classes and functions for correctness.

Add tests from the top down: Test whole subsystems as black boxes to see if their behaviour changes with changes in code. And so you can step through them to find out what's going on. This approach will probably get you the most benefit.

Don't be too concerned at first with what the "correct" behaviour is while you are adding tests, look to detect and avoid changes in behaviour. Large, untested systems often have internal behaviours that may seem incorrect, but that other parts of the system depend on.

Think about isolating dependencies such as database, filesystem, network, so that they can be swapped out for mock data providers during testing.

If the program doesn't have internal interfaces, lines which define the boundary between one subsystem/layer and another, then you may have to try to introduce these, and test at them.

Also, automatic mocking frameworks like Rhinomocks or Moq might help mock existing classes here. I haven't really found the need for them in code designed for testability.

Anthony
I think your answer is much better with the summary text: we can get more detail from the links if we want, but we can understand your position without reading through.
Dave DuPlantis
+2  A: 

Testable code is easy to spot - by the accompanying tests. If there are some, it must be testable. If there are none - assume the opposite. ;)

That said: Test Driven Development (TDD) is not so much a testing strategy as it is a design strategy. The Tests that you write first help in designing the interface of your classes, as well as in getting the scope of your classes (or subsystems for that matter) right.

Having the tests that you created during TDD and executing them later makes good tests, but is merely a (very welcome) side effect of that design philosophy.

This said, expect some resistance from your code against being tested. Listen to your code and change the interface in order to be easily testable. You'll most likely redesign it when you start writing tests.

Olaf
I don't know about resistance to testing, but I've managed to re-expose a bug that was earlier brought forward by the VS form designer a while back that was giving me a heck of alot of troubles by writing a test to cover that possibility. Yay!
Matthew Scharley
+8  A: 

Working Effectively with Legacy Code is my bible when it comes to migrating code without tests into a unit-tested environment, and it also provides a lot of insight into what makes code easy to test and how to test it.

I also found Test Driven Development by Example and Pragmatic Unit Testing: in C# with NUnit to be a decent introduction to unit testing in that environment.

One simple approach to starting TDD is to start writing tests first from this day forward and make sure that whenever you need to touch your existing (un-unit-tested) code, you write passing tests that verify existing behavior of the system before you change it so that you can re-run those tests after to increase your confidence that you haven't broken anything.

David Alpert
+1  A: 

Your DLL provides some sort of service. For every service, what do you have to do before getting this service, what parameters should you pass to get this service, how would you know that the requested service has correctly be executed ?

Once you have the answers to those questions, you can write a first test. Such tests would rather be called Characterization tests than unit tests, but would probably be easier to write than unit-tests if the DLL was not developped using TDD.

Characterization tests are also discussed in M. Feathers' "Working Effectively with Legacy Code", which is recommended in other responses.

Also, be sure to write a failing test before to add any new line of code.

philippe
+3  A: 

I call it "Test Driven Reverse Engineering".

Start "at the bottom" -- each class can be separately examined and a test written for it. When in doubt, guess.

When you're doing ordinary TDD in the forward direction, you treat the test as sacred and assume that the code is probably broken. Sometimes the test is wrong, but your starting-off position is that it's the code.

When you're doing TDRE, the code is sacred -- until you can prove that the code has a long-standing bug. In the reverse case, you write tests around the code, tweaking the tests until they work and claim the code works.

Then, you can dig into the bad code. Some bad cade will have sensible test cases -- this just needs to be cleaned up. Some bad code, however, will also have a test case that's senseless. This may be a bug, or clumsy design that you may be able to rectify.

To judge if the code's actually wrong, you also need to start at the top with overall test cases. Live data that actually works is a start. Also, live data that produces any of the known bugs, also a good place to start.

I've written little code generators to turn live data into unittest cases. That way, I have a consistent basis for testing and refactoring.

S.Lott