My Guide To Effective Test Driven Development
When it comes to Test Driven Development (TDD), there are 4 kinds of developers:
- The kind that doesn't do it, and doesn't care about it
- The kind that doesn't do it, but is intrigued by it and would like to learn more<
- The kind that does it, but wonders why it's not as easy as everyone claims it is
- The kind that does it effectively
I don't spend time on the first category, and I consider myself to be of the last category so this post is targeted at the 2nd and 3rd categories. Hopefully, some of the stuff in this post will help you on your way to using TDD effectively. It's basically just a list of stuff to keep in mind while doing TDD. So without further ado, I present you with my guidelines to effective TDD:
Keep your tests small and focused
This is probably the thing that most people get wrong, which leads to all kinds of problems. Developing software is all about managing complexity. How can you effectively do that? By trying to keep things simple. No matter how smart or gifted you are, the more complex you make things, the easier it is for you to make mistakes. You do need a high-level picture of how things should work in your application or system, but the actual implementation is something that needs to point itself out while you're working on it. Always start small and simple. Think of one small feature, write a test for that, and then write the code to make that test work. While you do this, make sure you don't get sidetracked by other features that suddenly pop into your head. Make a note of them, and when you're done with the test and the code you were working on, proceed with writing a test for one of the things you thought of earlier. But the key is to keep adding small features (after having written small tests for them of course). Some of you are now thinking "but what if I need to add a big feature?". Well, then you need to break that 'big feature' down into smaller parts.
After a short while, you will have accumulated a bunch of small things that work. Because you implemented them in a focused way, and because you were forced to think about how you'd use those features in your test before actually implementing them, you normally should've ended up with a bunch of little parts that are easy to use. And your tests already prove that they work. That means you can start using these parts (they can be classes or just individual methods) to get other things (new features) working. Whenever you use a part that you already finished earlier (including its tests of course, since you wrote them before you wrote the finished code of the part) you don't need to worry about the correctness of that part in the tests that you're writing for whatever new feature you're working on. This is something that a lot of people get wrong. I can not stress this enough: the correctness of dependent code should be covered by its own tests!
I believe the approach outlined above really is the key to effective TDD. Obviously, there are more guidelines, but this is the one that will have the biggest impact on your experiences with TDD.
Use dependency injection
Dependency injection (DI) is a really simple technique that offers more advantages than simply making TDD easier to do. But the focus of this post is on TDD, so I won't get into the other advantages. You can find an introduction to this technique here.
Keep your tests fast
A slow test is like body oder: the problem is only gonna get worse until it's taken care of properly. If you have a bunch of smelly people in one room, you really don't want to be there. It's the same thing with a bunch of slow tests... nobody wants to be around those. That means that most people won't run the tests frequently, which is a shame. So you really need to keep those tests fast if you want people to run them often. The best way to achieve this is to mock/stub/fake your dependencies in your tests. If you've used DI properly, this is really easy to do. Every dependency that can slow down your test, or make it unreliable because of factors that are external to the test, should be mocked/stubbed/faked. Some of you might be wondering: "but how can we be sure that the code really works when it's using the real dependency?". Well, the real dependency should be covered by its own suite of tests that thoroughly tests the behavior of that code. If the correctness of the dependency is properly covered by its own tests, why on earth would you subject yourself to the downsides of using the real dependency in tests that have to cover other parts of code? Using mocks/stubs/fakes allows you to keep your tests focused on that piece of code they actually should cover and it helps in keeping your tests fast. What's not to like?
Minimize access to the database in your tests
A lot of people use their database in a lot of their tests. This usually leads to slow tests, and excessive test set up to satisfy all relational constraints which in turn leads to fragile tests. None of this is beneficial to your productivity. But of course, you do have to test the code that uses your database. The key is to write tests for each specific database activity (a query or any other kind of statement). Write tests that verify that a query returns the correct values. Write tests that verify that a query does not return incorrect values. Be sure to cover everything you want to cover to make you feel secure about that query. And stop right there... every other part of your code that needs access to this specific query should be talking to an object (a dependency) instead of performing this query itself. Use mocks/stubs/fakes instead of the real dependency in the tests of that other code. By replacing your real data access code with mocks/stubs/fakes in your tests, you can make your tests very fast, and you can usually simplify the set up of the tests as well.
Don't over specify your tests
If you're using mocks, be sure not to write tests that are too tightly coupled to the implementation of the code you're testing. If you frequently have to modify your tests because you changed the implementation of the class without it affecting the observable behavior of the class, then you probably have over specified tests. Mocks can be a very effective tool to increase both your productivity and the quality of your tests, but when used wrong it might lead to a lot of unnecessary extra work. This is quite an extensive topic in its own right, so I can't do it any justice within the context of this post, but it is something to keep in mind even if you don't have experience with mocking yet.
Don't be afraid to throw tests away
If you're doing some heavy refactoring, you might break a few tests. Depending on how extensive the refactoring is, you might break a lot of tests. A lot of people feel that they have to keep those tests around... but that's not always the case. Take a close look at the tests... Do they still make sense after the refactoring? Perhaps you need to refactor those tests as well instead of going through hoops to get all of them working again. Sometimes you may need to modify a few tests to get them working again... But sometimes, those tests simply aren't valid anymore. If they are, throw them out instead of wasting time on them. If the assertions of the tests are still important, but it's too much work to modify the tests, it may actually be less work to simply rewrite them against the refactored code. There is no one right answer here, but you may need to learn how to look at your tests from more angles then you may currently be used to doing.
This is by no means a definitive list of things that you need to keep in mind while doing TDD... But I do think these guidelines may be the most important ones. If you follow these guidlines, you should end up with fast tests that are easy to understand, not a pain in the ass to maintain, and most importantly: verify the correctness of the code under test.
Once you have that, the code and design of your system is probably pretty flexible and it will be easier to adapt as time goes on. And this is probably the single biggest advantage of doing effective TDD.
It might take you a while to learn how to do all of this effectively. But that's ok... there's nothing wrong with that at all. Baby steps are better than no steps at all, right? Just keep in mind that if doing TDD hurts, you're not doing it right... yet. Figure out where the pain comes from, try to fix the cause and try again. It's a repetitive process and you wont get it right immediately. None of us did.
Written by Davy Brion, published on 7/14/2008 9:38:16 PM