Test-driven development is an essential discipline for any software craftsman. Similar to how accountants use double-entry bookkeeping as a safety net, software developers can use TDD as their safety net.
If you need a refresher on TDD — and what it is and isn’t — I’d suggest my post on why Simply Writing Tests Is Not Test-Driven Development.
1. Think of Tests as a Sum of 4 Parts
When writing unit tests, it helps to think of them as 4 parts — setup, invocation, assertion, and tear down — and to consider each step as a series of questions. I start with the invocation first.
- What function am I testing?
- What parameters does it take?
- What should happen when this function is invoked?
- What am I expecting the function to return?
- What collaborators does this function and object work with?
- What is needed to see the call through a return value and assertions?
- What do I need to undo from setup?
- Is there anything else that will interfere with other tests, events or state?
2. Refactor Tests Too
Unfortunately tests are often neglected when it comes to the refactor stage of “red, green, refactor.” Tests are not a not a second-class citizen. Quality is a critical measure for our test code, and the broken window theorem applies here too. Your production code depends on these tests, and thanks to them, you can be fearless in improving the quality of your production code.
Some helpful tips for refactoring test code:
- DRY it up! Move common code, often your setup and tear down, into before and after blocks.
- Write helper functions to remove duplication.
- Always keep your eye out for smelly and awkward test code.
- Ensure the tests aren’t testing too much.
- Make sure failures return sensible messages.
3. Test Behavior, not Implementation
Make sure your test reaches as far as makes sense. If you are testing that a backbone model makes a REST request, don’t mock out the fetch method on your model. There are two problems with doing this: first, you’re testing the implementation; second, you don’t own the fetch method and shouldn’t mock it. The proper way is to mock out the server and assert that the behavior is correct. Check that the URL is hit with the appropriate parameters.
4. Understand Spy vs Stub vs Mock
Testing object collaboration can be tricky. Become familiar with when to spy, when to stub, and when to mock.
- Spies are usually good for testing callbacks and the publish side of pub/sub events. Keep an eye out for over usage and spies that feel like they should be a mock instead. I usually start to get on the defensive about spies when I find myself wanting to investigate and assert on the parameters of a spy. Is it really a partial mock in disguise? Will it be okay if my original function is invoked?
- Stubs are good for driving down a certain execution path. They should not have a direct effect on whether the test passes or fails. An alternative usage of stubs is to prevent a method from being invoked at all.
- Mocks can be thought of as a combination of stubs and spies. The general rule of thumb is to only mock what you own. Mocks do have a downfall though, they can quickly deviate away from the actual implementation of what you are mocking. It is essential that your unit tests are wrapped by an end to end acceptance test, this will catch any mocks that have gone rogue. The last tip is, if the mock should not fail your test, it should probably be a stub.
What are some of your favorite tips and tricks for test-driven development?