What is TDD?
Test driven development, known as TDD, is a style of programming which is based on the idea that you should write the test for a feature before you write any production code.
The 2 rules which govern TDD best practices:
Write only enough test code to produce a failing unit test
Write only enough production code to create a passing unit test
The idea is that you develop the test in parallel with the production code. You write a little bit of the test, then write enough production code to make that part pass. Then think of the next thing to test, and write the production code to make it pass. This keeps you thinking through the entire process, and helps you create less buggy code.
However, it seems like over the years, the best practices and foundational beliefs held by people who practice TDD have become muddied.
How to do TDD well
Remember a few key facts as you read the following paragraphs:
- The purpose of tests is to allow you to refactor your code and remain confident that you haven’t broken something else while doing so.
- A unit test should be independent of all other tests. The outcome of test 1, should not affect the outcome of test 2 in any way.
Write tests for behaviours, not the implementation details
If you are creating an object which deals with converting currencies, then the behaviour you expect from that class is “convert X currency to Y currency”. This functionality will likely be accessible via a public method. This is the method that should be tested against, because this is the part of your code that will be consumed by other parts of the system.
However, if that same object contained 9 other private methods, which handle the implementation details of how we convert the currency, these should not be tested.
This sounds like blasphemy, however the reason is that in the future, you are very likely to need to refactor how currencies are converted. The design of the code will almost certainly change. If you have wrote tests for these implementation details, then the tests will now likely fail as well, rendering your test suite useless.
It now becomes more time consuming to re-write all the tests. It also means: instead of being a fail-safe, the tests are just another barrier to refactoring.
A unit is not necessarily an individual class
The reason testing is so difficult is because we are obsessed with test coverage. We assume every method we write, needs a corresponding unit test. In order to achieve this, we need to write tests cases for methods within a class. Since we know that tests must be independent, this means we have to mock an environment so that we can test that class method in isolation. Now mocking is a dark art, which leads to massive complexity and very hard to manage tests.
The better practice, is to remember that we are testing behaviours, and not every single method. We should design our code in a way that exposes only the methods that are required to achieve the behaviour needed. Then, the need for mocking reduces dramatically, and tests become easier to maintain.
Red Green Refactor
The theory behind this is that you cannot focus on solving the problem and writing clean code. So focus on solving the problem in the fastest possible time, then refactor the code.
- Write a failing unit test — Red
- Write enough production code to make the test pass in the quickest, dirtiest time possible ( You now know how to solve the problem ) — Green
- Refactor your dirty solution to a clean solution, without breaking the test
Bad things about unit testing in its current form
- You have to rewrite tests every time you refactor
- You spend more time writing tests than writing code
- Developers resent the tests because they provide no real value
How to do testing well
- Test the behaviour not the implementation details
- Adopt a “Given, When, Then” model for testing. “Given my account has 100 when i add another 100 then i have a balance of 200”
- Remember that changing implementation details = refactoring, but changing the public facing API methods = change. Think very hard before changing these public methods.
- Make sure your tests run independently from each other
- Not every class is a unit
- Perfectly okay to test against a DB, provided it doesn’t affect the speed of the tests too much
- Red Green Refactor is an excellent way to write tests
- Avoid acceptance tests — End users don’t care, and they don’t engage with them. The developers hate writing them as they are time consuming.
- Avoid large amounts of user interface testing. These are time-expensive to maintain, because as soon as you change the UI at all, you have to rewrite all the tests. They also take a long time to run ( potentially overnight ) , so you do not get instant feedback. Every morning is spent figuring out who broke the tests.
- Most testing, should be unit testing.