For the past few weeks, the topic of my book club has been Unit Testing by Vladimir Khorikov.
It's an intermediate+ level book on automated tests, mostly focusing on unit tests. Fairly opinionated, but I found myself agreeing to the core of pretty much every claim, so I mean, that's good because I fairly opinionated myself.
In the book, the author presents four core characteristics of a good unit test.
- Protection against regressions - Does it catch as many defects as it can?
- Resistance to refactoring - Will the test require updating alongside the production code?
- Fast Feedback - How quickly do they run?
- Maintainability - Is the test easy to setup, run, and update?
Protection against regressions
Arguably the main point of writing tests at all. We want to know when something doesn't work. But it's a sliding scale of course, some tests are better in this regard, and some are worse. Tests with many Asserts generally score better, because it nails down the behavior more, and tests that Assert on nothing, are worthless. (At least in compiled languages. Tests that assert on nothing in interpreted languages still protect against typos.)
Resistance to refactoring
In my opinion, the most important facet. You want your code, and this includes your tests, to work on a high level of abstraction, so that you can easily change the details of things without needing to rewrite everything.
Fast Feedback
Unit Tests can cover a lot of the code base really quickly. In a good testable system, you can run hundreds of tests in less than a second. This is is very important for the Developer Experience and workflow. You need your tests to not move out of the process boundary, not touch the network or hard drive, be parallelizable etc.
Maintainability
Lastly maintainability touches upon a lot of the other things, like if there's a separate program you need to install or run, be it a database or such. How hard the code is to read.
Key takeaway
Vladimir does a nice job explaining mocking, and how to push the mocking to the very edge of the system in order to gain maximum rewards from it. He then completely invalidates mocking as a tool by introducing spies, and show how they can give you a better experience. I too have championed for interfaces at the edges, but thanks to this book it was taken to the next level. And this touches on the bigger picture idea that you should design your code to be testable like this, and refactor/rewrite it to enable these sorts of things.
One more thing
When the author talks about testing the database, he says quite categorically, "Do not use in-memory databases". I too have had that come back and bit me. But the lesson I learned was that relying on an in-memory database, and assuming that it matches the production one is a flaw. So I think a test suite that works with both an in-memory one, and a "real" one is not inherently evil. This is a particular pinch of the salt you might want to apply to more of the book. Be aware of your assumptions.
All in all, a great book. Do read it.