A big part of writing good automated tests is understanding why to write each test. While this seems obvious, people rarely think about it. When I write a test, I make sure that it achieves at least one of the following goals:
- Verify that the product meets the business requirements
- Give cover for future change
- Document intent
Verify that the product meets business requirements
This is the most obvious and most traditional goal of automated tests. Fundamentally, we need to make sure that the product works correctly. Most tests are justified by this requirement. In fact, many developers use this goal to justify writing bad tests by thinking in terms of making sure the code does what they expect it to, instead of verifying that the product meets business requirements. Following that logic, tests that verify code flow and tests of trusted language features (getters and setters for example) should not be written. Instead, focus tests on the inputs and outputs and verify that the expected business logic was executed. This will, implicitly, verify the code flow but avoid unnecessary coupling that results in too much test maintenance.
Give cover for future change
In the book “Working Effectively with Legacy Code”, Michael Feathers defines legacy code as “code without tests”. The bulk of his book deals with creating and using seams to insert tests before changing or refactoring code. The second goal of testing is to build code that can be safely changed in the future. This means leveraging a good mix of integration and unit tests to build stable test suites. It also means that tests should be coupled to expected outcomes instead of tightly coupled to code behavior. Modules should be replaceable without having to rewrite all of the tests that govern behavior. Some related unit tests may be removed and replaced by unit tests for the new implementation, but a shell of integration and other loosely coupled tests should remain in place to provide cover.
This is the least-often invoked but one of the more important goals of tests. The tests we write should be a living document communicating what we believe the production code should do. Gaps in test coverage lead to, at best, confusion for future developers reading the code. As my friend James Spargo says: “code like the next developer is a homicidal maniac who knows where you live” to which I say “or who might be you”. Tests are no different – three, six, or twelve months from now you or a colleague may be called on to enhance or debug your code. Having clear, concise and complete tests might just make that job easier.
There are certainly a few other cases for testing – contract tests are an invaluable tool for exploring a new API, then ensuring that it keeps working. Performance tests are a class unto themselves. But for most tests that we write ever day, using these goals will improve your tests, and consequently your product and your ability to deliver working software faster.