After using TDD at work for about 2.5 years, I finally feel that while I still need more practice I am no longer a novice. I am confident enough to post some advices that come from common pitfalls I’ve encountered when still at beginner level. While they might be obvious to TDD experts I often notice similar mistakes other beginners make. So I thought that I might write as well them down.
Make sure your tests are short
A test that is longer than a screen page is an indication that you are either testing too many aspects in one test case or did not spend enough time to refactoring your test. In my opinion, the fact that a test is longer than 10-15 lines is an indication that there might be need to improve the test.
- I find it useful to use the Arrange Act Assert Pattern to give my tests an uniform and simple structure.
- I like to use explicit comments like // arrange, // act // assert to make sure a reader can take a quick grasp of the test structure.
- I often extract lines of code that deal with arranging mocks into SetupSomething()-Methods to make the test both shorter and hide unnecessary implementation details.
- I use Test Data Builders for arranging complex data types or avoiding overspecifying irrelevant information1
Don’t be satisfied with the test because it just works. Never give up refactoring.
Give your tests good names
I often stumble at test names like PrintTest(). What the heck is that test doing? It clearly says that it tests the printing functionality, but what is the expected outcome? Are there given additional constraints for the print functionality, such as a special type of document to be printed?
In most cases when I am forced to look up an already existing test it is because it either fails or I need to tweak the SUT to add some new functionality. I don’t want to waste any time to reverse engineer the test just to understand what it is doing and why.
- A better test name might be InvoiceToBePrintedShouldBeTemporarilyStored().
- It indicates that the document (in this case an invoice) should be temporarily stored.
- It describes both the scenario and the expected result.
- It also forms a whole sentence without any need for the reader to use some magic language to identify scenario, constraints and the expected outcome like Print_Invoice_TemporaryFile()
Find a good naming scheme for your tests and stick to it. Avoid any abbrevations or cryptic languages that would undermine the understanding.
Make your assertion failures provide enough information for further analysis
I get furious when a test fails and all I get is
PrintTest() failed. The message was: expected true but found false (some other stuff about the line of code and two pages of stack trace information)
This forces me to read the whole test. Note that most test runners give you only minimal information just as in the example above. What if my test contains more than one assertion check that something should be true? What is the meaning of true in this context? Is the return value of a called function verified? What function is called?
Remember that I am seeing the assertion message only when something is wrong with my software. I want to correct this as fast as possible and don’t need any additional tension that comes with deciphering unclear assertion messages.
Now compare the above failure with following message (all I did was tweak the name of the test and the assertion message).
InvoiceToBePrintedShouldBeTemporarilyStored() failed. The message was: expected that the file "/tmp/printinvoice1234.tmp" does exist but it doesn't (anything else that follows is of miner concern to me to get a grasp what went wrong)
Without any peek into the test code I see that the test was checking the existence of a temporary file but it failed. In the best case this might suffice to find out what went wrong. At least it will make the debugging easier.
Add enough context information to analyze a failing test. If you stumble across a failing test that does not provide enough, improve your assertion messages.
Use TDD for the right reasons
In my opinion, TDD should be used to improve the overall code quality, protect your code from regressions and document what your code is doing and why2.
Don’t use TDD because of reasons. Don’t use it to achieve some questionable goal just like a 100% test coverage3. Don’t use it because it is the hip way to code nowadays. Don’t use it because someone (smart guy/management/whoever) told you you have to. Don’t be a cargo cult programmer.
Use it because you believe in the goals mentioned above. Use it because you believe that your current state of code is not good enough yet to achieve these goals. Finally, use TDD to make the life easier to the future maintainer of your code, regardless if it is another person or just your future self.