I’m available for freelance work. Let’s talk »

Careful with negative assertions

Sunday 4 November 2018This is over five years old. Be careful.

A cautionary tale about testing that things are unequal...

We had a test that was kind of like this:

def test_thing():
    data = "I am the data"
    self.assertNotEqual(
        modify_another_way(change_the_thing(data)),
        data
    )

But someone refactored the test oh-so-slightly, like this:

def test_thing():
    data = "I am the data"
    modified = modify_another_way(change_the_thing(data)),
    self.assertNotEqual(
        modified,
        data
    )

Now the test isn’t testing what it should be testing, and will pass even if change_the_thing and modify_another_way both return their argument unchanged. (I’ll explain why below.)

Negative tests (asserting that two things are unequal) is really tricky, because there are infinite things unequal to your value. Your assertion could pass because you accidentally have a different one of those unequal values than you thought.

Better would be to know what unequal value you are producing, and test that you have produced that value, with an equality assertion. Then if something unexpectedly shifts out from under you, you will find out.

Why the test was broken: the refactorer left the trailing comma on the “modified =” line, so “modified” is a 1-element tuple. The comparison is now between a tuple and a string, which are always unequal, even if the first element of the tuple is the same as the string.

Comments

[gravatar]
Other ways to have an infinite space of unintended passes:

Asserting that an exception is thrown, without checking that the exception is the kind you expected.

Making an assertion within a callback, which is never called, or which is called by something that catches all exceptions including the one the test framework uses for "assertion failed".
[gravatar]
This is why the TDD cycle specifies "red" then "green" : run the test and see it fail first, as a sanity check of your test, even if you have to add a throwaway line to your code to make that happen. Once you've seen the test fail, then replace the throwaway line with the real implementation to see the test pass.

Once you start doing this by habit, you will be *amazed* at how frequently you discover you accidentally create tests that pass *all the time*, no matter what the code-under-test says. Thus proving that this habit is vital.
[gravatar]
Mutation tools also comes at hand to spot these failures.
[gravatar]
That sort of tuple accident comes up in various contexts. We use this flake8 plugin to catch that sort of thing. https://pypi.org/project/flake8-commas/
[gravatar]
There are two parts in this problem:
* we should be careful with our assertions, especially the negative ones (because of the wide range of valid values)
* we should be careful when refactoring our code. Refactoring is necessary. But refactoring untested code (like the code of the test here) is always dangerous, and extra care should be taken in this case. And specifically, you need to double check anything you copy and paste.

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.