Having established what tests can actually be good for, outlined the only two types of test that matter and their value, and having attempted to make the case against a narrow fixation on “unit tests”, where does that leave us?
Well, it leaves us free to think about how we can write good tests that are useful and practical! And the formula for that is very simple:
Every feature, bug fix, coding task, etc. should have one or more clearly stated requirements. Not how this code is to be written, but what does this code have to do? Often, the requirement can be described from the perspective of the human who is using the app, e.g. “Tapping on the star should add this items to my list of favorites”. Requirements can also be described purely from a API perspective, e.g. “When there are already 100 favorites, adding an additional item to the favorites list should result in an error and no change to the existing favorites”.
Each API requirement should be validated by an API test
Each UI / user requirement should be validated by a UI test
Every test should test the required behavior, not a specific implementation. If the implementation changes (whether the code underlying the API, or the layout or styling of elements in a UI), the tests should not need to change. In fact, the tests exist largely to validate that a change to the implementation still meets the same required behavior. So tests must only be concerned with the behavior, and not an implementation.
For UI tests, this means validating and interacting with elements by identifier rather than looking them up by a specific screen or expecting a specific hierarchy. For API tests, this means that writing code and tests that address protocols / interfaces and abstractions, which can be explicitly passed mockable inputs, and validate explicit outputs (via mock expectations or return types) rather than digging into properties and internal state.
And that’s the entire formula. It’s kind of neat that the entire approach to testing (at a high level) can be boiled down to just these few points. But you’ll notice that every test you write using these simple principles will have value and align with the project objective. No time is spent writing tests that don’t validate an explicit requirement. Any deviation from required behavior (whether through refactor or the addition of new code) will cause tests to fail, easily proving their value. And as you gradually capture more and more and more of what your application does and needs to do as explicit requirements (more on that in an upcoming article), your test suite is giving you direct confidence that your code is meeting those requirements on every run.
Of course there is a lot more in the details (which we will continue to dig into with additional articles), but those details largely boil down to how to write good requirements, how to make tests more abstract (test the behavior, not the implementation), and perhaps some specific techniques for providing inputs and validating outputs.
There is a ton of room of creativity and mastery in these specific areas, and lots of work to do in capturing ALL the requirements of your app to test, but hopefully this starting point of a useful and practical approach to testing helps you focus on the parts that matter, and gets you the most bang for your testing buck!
You might also be interested in these articles...