Endless Parentheses

Ramblings on productivity and technical subjects.

profile for Malabarba on Stack Exchange

What tests you shouldn’t write: an essay on negative tests

Software tests are great! I’m fortunate enough to have only worked with code-bases with reasonable-to-excellent test coverage, and I wouldn’t want to work in a world without tests. In fact, a thoroughly tested system is nothing short of liberating.

That said, tests are not free. I’m not talking about CI time, that is a cost but it’s usually reducible. Nor am I referring to the effort it takes to write the test, that’s a very real cost, but people are usually very mindful of that (it’s easy to take it into account the very real effort you’re having right now).

The cost that people tend to underestimate is the time wasted with false failures.

Let’s get the basics right. Tests are designed to fail. A test that never fails under any circumstance is a useless test. But there are good failures and bad failures. Which gets me to the entire point of this post.

Write tests with real failures

Real failures are good, false failures are bad.
You want a test to fail when you break functionality, not when you harmlessly change code.

Let’s start with a quick Ruby example. Consider a model with an attribute called value. It wouldn’t be surprising to see a test like this for such a model.

it { is_expected.to respond_to(:value) }

If you don’t know Rspec, this is roughly testing that you can call .value on the model.

But when will this test ever fail?
Under any reasonable condition, this will only fail if you rename the column in the database. Nobody will ever do that by accident!

What’s worse, this failure will never carry any useful information. It doesn’t tell the developer that this change is unexpectedly breaking something. All it ever does is give us yet another piece of code to fix while in the middle of an already long refactoring.

And how do we fix it? By editing the test to use the new name.

A false failure is one that you fix by editing the test, while a real failure is one you fix by editing the code.

False failures are unavoidable. Every test is exposed to them, and every time they happen the developer wastes some amount of time identifying and fixing the failure. That is a negative cost that every test carries and not everyone takes into account.

For most cases, we happily pay this cost because not having a test is way worse than having to fix it. Because one real failure preventing a bug from going live outweighs several false failures, adding up to a positive net effect.

But some tests (such as the example above) virtually never have real failures. Without any positive upside to them, they are strictly negative tests.

And how do we avoid negative tests?

Test function, not code

Let’s expand on our previous example.
Rails provides a helpful one-liner to validate the presence of a mandatory attribute.

validates :value, presence: true

That’s fine and good. The problem is when you see a similar one-liner testing that validation.

it { is_expected.to validate_presence_of(:value) }

Pause on that for a moment. We’ve written a spec to test a single line of code.

The only way that can fail is if we rename the attribute or remove the validation. Again, nobody is ever going to do that by accident. We’re not really testing that a specific functionality works as it should, we’re just testing that a particular line of code is written in a particular way.

That is a code test, not a function test, and code tests are negative tests.

A function test is one that verifies non-trivial functionality, functionality that could be accidentally broken by a number of reasons.

Testing the interface of a service, for instance, is basically always good. As there’s usually at least a few branching code paths inside it where one could inadvertently break a branch while editing another or while adding functionality.

Unit tests for simple functions and methods, in my opinion, are not no-brainers. People like to go nuts with them, because they’re easy to write and quick to run (so “why not?”), but a lot of them fall under the category of negative tests.

Unit tests are good when testing some reasonably complicated algorithm, as someone could actually break an edge case while trying to optimize the implementation. And even then, you shouldn’t just write a couple of mindless tests, as they will probably be negative. You should put some effort into figuring out the edge cases and testing them specifically.

Think before you test

Hopefully, you started thinking well before you wrote that first line of code, so there’s no reason to stop now just because you changed from the app/ to the specs/ directory.

Thinking and being mindful of what you’re testing will not only help you avoid negative tests, but will go a long way to making your positive tests more effective at catching the bugs they’re supposed to catch.

Tags: testing, essays,

comments powered by Disqus