This is one of the most important lessons I try to teach less experienced developers. Next to making sure that your code works now, it's also essential to make sure that developers can fix defects in the future. The first step to achieving this is to write tests. Preferably, you are writing the tests before you write the code. But even if you have done that, your tests might still be inaccessible to other developers or make it hard for them to figure out what broke. Here's my list of properties that make tests go from good to great.
TL;DR
Mixed concerns in tests
- figure out the number of concerns the test handles,
- how they relate to each other, and
- which one causes the trouble.
in software engineering is the what in "What does this code do?".
If your code reads from the command line and writes to a database, you are handling at least two concerns.
When you are testing your code, it's important to test one concern at a time.
When a test fails, you have a much clearer picture of what aspect of your code isn't working correctly anymore.
If your tests are handling two or more concerns simultaneously, you have to do some extra work before you can even start looking at what is wrong.
You need to:
All this extra work takes time which might be critical depending on the defect.
I follow a simple rule of thumb. Whenever I write the word "and" in a test description, I write two tests instead. Let's look at an example.
describe("Database manager", () => {
it("should support reading and writing to the database")
})
This might be a great test that fails when something with the database connection is wrong. But could you tell which part has a problem? It could be the part of the application that reads the data, but it could also be the part that writes it. Even worse, it could be both. We can get out of this situation by splitting this test into two.
describe("Database manager", () => {
it("should support reading from the database.")
it("should support writing to the database.")
})
Now when the first test fails, we know something is wrong with the code that reads data. Respectively, we know that when the second test fails, the code that reads from the database has a flaw.
For me, not combining tests is hard when I'm starting on a new feature, and all the different use cases I need to test pop into my head.
In these situations, I use to-do tests (I mainly work with jest
).
To-dos help me track what I still need to implement without bloating the existing tests that I have already written.