Thursday, September 25, 2008

TDD: Fail fast

It has been brought up numerous times over the last few months about the failures of TDD for the masses. It has been proposed the the masses are not in fact test driven. It seems the majority of these failing to get the benefits of TDD fall in a couple of camps.

One camp is throwing in the occasional test if it happens to cover an area of concern. This is usually not Test Driven but retro fitting tests.

Another camp is the "over testing" camp, going for the "holy grail" of high code coverage often leading to:

a) Tests that don't test anything that needs to be tested. Common examples I see are testing frameworks you didn't write (i.e. NHibernate or System.IO) or testing property assignment worked etc

b) Brittle unmaintainable tests that soon become a mass of failings tests.

For some reason it appears the barrier to entry to real TDD is too high. What I plan to cover here is all the mistakes you will probably make when picking up TDD. By acknowledging these mistakes early, hopefully you can over come them and improve the way you approach testing.

  1. The first mistake many make is not knowing at all what they are doing. Get Kent Beck's TDD book and read it. Don't stop at the 3rd chapter, it really is one of the easiest books I have read in the last few years. It a quick read, in plain English and cover 80% of the tests you will be writing. It amuses me that people are too pig headed to learn how to do something correctly that they are supposed to be doing all day (and are paid to do!)
  2. Decide how you are going to be using tests as part of you design process. Be honest with yourself. If you know that you are not going to be truly TDD and write tests first then don't pretend you will. No one is watching over your back, karma wont bite you. But seriously if you haven't tried it, start up a side project at home and give it an honest bash. Make the mistakes on something that "doesn't matter" so there is a boss looming over you as you rewrite brittle tests. If you are not going to use TDD you can probably stop reading now. :)
  3. Make TDD part of your evolving, agile design process. TDD for me now is also a major part of my design process and one of the key reasons I use it. I generally have an idea of what I want but often TDD pushes me to a better design than I had first envisaged. Classes and methods become cleaner and more focused, interactions more expressively conveyed.
  4. Decide if you are going to use doubles*. This is an extra aspect of TDD to learn and is usually where things go pear shaped. Not using doubles in an enterprise environment usually means you are doing integration tests, test that call multiple classes, methods and layers. Integration tests can be slow as they often interact with databases, file systems and other services. They can also be brittle as they rely on the environment being in a constant state. Changing something in the database or a class several layers down may incorrectly break a test which means test that are hard to maintain.
  5. Understand Doubles. Whether you use them or not you should try to learn to understand what they mean. Doubles help you write Unit tests when a piece of code has dependencies. A unit test should test a unit, not all the stuff hanging off of it. I am not going to go into great detail here as it is covered by Kent, Martin, Roy, Gerard and the rest of the TDD community with an opinion. The two I use most commonly is the Mock and the Stub. A stub is a place holder that returns stuff when you call a dependencies specific method. Stubs don't cause test to fail, they just allow them to proceed with a more shallow dependency than you would otherwise use. A mock, like a stub, mimics a dependency in returning predefined results from a specific call, however mocks can break a test. Using a mock you are saying I EXPECT this method to be called on this dependency and if it is not, this test should break. This is where lots of people go pear shaped. People tend to "Over Mock". If it is not critical that a dependencies method is called then it is not a mock expectation, it is probably just a stub. See Ayende's write up on how Rhino Mock 3.5 moves to help with this. If you are not using Rhino Mocks, give it a go. The dynamic mock is worth it alone.
  6. Don't over Assert. An Assert is the test frameworks way of asserting the SUT's state. A good clean test will be short and ideally have one assert. Now this is not always the case, however if you are seeing tests with dozens of asserts becoming common place it is time you have a closer look at what you are testing.
  7. Don't over Expect. Following in the same vein as above, if you have more than, say, 3 mock expectations in one test you may need to re think your design as that is a lot of interaction for one piece of code. Again I try to keep my expectations to 1 or 2 per test.
  8. Run test often. Now many of you will be using Visual Studio Team System and therefore may be inclined to use MSTest (the built in test framework). That's cool, as long as it doesn't slow you down. For me, its way too slow. I am currently running a NAnt build script with MbUnit + RhinoMocks to build and test. This thing is fast and only runs my units test, not my integrations test. I run this script every couple of minutes, as that's how long it should take to either write a test or fill in the code to make the test pass. If your "build and test" turn around is more than a few seconds, you probably wont be doing it to often, which obviously affects you adoption to TDD. Some easy wins include: Having a smaller solution (with only projects you need in the build & test process), using a build script instead of VS to build & test and of course minimising your integration tests, focusing on faster unit tests. Its probably wise for me to say that I do have integrations tests, but I try to minimise them and only run them a couple of times a day, not every build cycle. A perfect time to run them would be as part of your check in process (which I assume you do reasonably often).
  9. When a test breaks: Tools down, fix the problem! If you have just made code changes and new Red light come on, this is bad! Only one red (failing test) at a time please! Enough said.
  10. Refactor! Refactoring  means better code. More readable and better performing sounds like great things to me, especially in the safety of a test suite to assure you the end result is still the same. It also highlights if you have brittle tests. When I first wrote tests I didn't refactor that much, as it often broke the tests. This was a mistake. Instead of "not refactoring" I should have addressed the root issue, which was brittle tests. Flying solo while doing this can be hard. That's what forums & user groups are for. Show the world your mistakes so you can stop making them**. You will probably find dozens of other doing the same thing and not realising it.
  11. Help team members get better at testing. Teaching others also helps you gets better as it highlights gaps in your own knowledge. The main benefit is the cross pollination of ideas and concepts with in your team. If one team member is spending a lot of time rewriting tests it is a sign that they are missing a concept. Maybe they are not using doubles correctly? maybe they are retrofitting tests
  12. Keep set ups and tear downs small. If you set ups are massive then your SUT is probably to coarse and need to be broken up to more manageable bite. Typically my set ups have a class fixture wide mock assignment (for fixtures that focus on the same dependencies) and teardowns only have a mock verification if I have one at all.
  13. Don't think TDD will come in a couple of hours. It doesn't. I have heard comments from others and tend to agree to make all the mistakes and come to the point where TDD is a natural progression takes about 6 months of standard 9-5 developer time. If you are an uber nerd maybe shorter as you may be writing some code at home or you just bash out more or you get concepts faster. I wrote my first NUnit test about 18 months ago and first played with A mocking framework 5-6 months later. for most of that time I was running blind and had really, no idea what I was doing. only in the last 6 months have I become very confident in my TDD ability, by reviewing my and other people tests can you see where and why you are doing things wrong. I am not the TDD guru but there is not much that I write now that I cant test (that I want to test!)

Although this is by no means a comprehensive list, it does give you pointers as to where you may have problems with your tests. I figure I would rather fail fast (if I am going to fail at all) so hopefully these pointers give you an idea to which paths you should not be going down.

As a side note: It is now habit writing tests as I go. This doesn't come at first but pushing thru the initial barriers and you will get there. I don't always follow the "Red, Green, Refactor" as sometimes I just write the test, write the code and run green. But when I first started I found the red aspect helpful.

Hope this helps get someone on to the TDD bandwagon a little easier and little faster.


*Doubles include fakes, mocks, spies, dummies and stubs.

**Not all your mistakes, then the world just thinks you are a clown

No comments: