Don’t write Conditional Logic in Tests

Conditional logic should not go in tests

This is one of the most important rules in writing good tests.

You should be suspicious of any test that includes constructs like if, while, for, try or anything similar that your language might have.

There are two main reasons for this

1. It forces you to read the code

I shouldn’t have to read the code to know why a test failed

import {expect} from 'vitest';
...
test('JacketAdvisor', () => {
    ...
    if (weatherOutside === 'cold') {
        expect(sut.shouldBringAJacket()).toBeTruthy()
    } else {
        expect(sut.shouldBringAJacket()).toBeFalsy()
    }
}) 

The above code is reasonably easy to read, but if the test fails I will get this message

FAIL  path/to/tests > JacketAdvisor
AssertionError: expected true to be falsy

and I’ll have to open the code to figure out why it failed. Each case should be handled in a different test


import {describe, expect} from 'vitest';

...

describe('JacketAdvisor', () => { 
    test('with cold weather shouldBringAJacket returns true', () => {
        ...
        expect(sut.shouldBringAJacket()).toBeTruthy()
    })
    
    test('without cold weather shouldBringAJacket returns false', () => {
        ...
        expect(sut.shouldBringAJacket()).toBeFalsy()
    })
})

This way when the test fails I’ll have something like this:

FAIL  path/to/tests > JacketAdvisor > with cold weather shouldBringAJacket returns true
AssertionError: expected true to be falsy

And I’ll know immediately why the test failed.

2. It introduces untested code

This is maybe the more insidious problem. If you have untested logic that you use to test your code, you have no tests that confirm you are actually making the correct assertions.

For example if I accidentally set weatherOutside to 'col' in the second example above, the first test will fail. But in the second example, this code expect(sut.shouldBringAJacket()).toBeTruthy() will just never run. I won’t know that the code was wrong until it hits production because the test code was wrong and had a bug.

But I need conditional logic for some assertion

Most of the time when you think you need to add conditional logic what you really need to do is properly isolate your system under test. But there are exceptions where it does make sense to introduce conditional logic.

1. You’re working with legacy code

When it comes to legacy code all rules go out the window. If you need to add a bit of logic to set up a test harness and make it possible to safely refactor that code, then just do it. Don’t let the perfect get in the way of the good.

Just remember that it’s temporary. When you’ve refactored the system to the point where it’s components can be tested in isolation test those components and delete the old tests.

2. Your writing a testing library or a custom assertion

It’s actually impossible to have a test with no conditional logic because under the hood all assertions look something like this:

function assertTruthy(val) {
    if (val == false) {
        throw new AssertionError({message: 'expected true to be false'});
    }
}

Instead of “Don’t write Conditional Logic in Tests” the rule should probably be “Test code should never have logic that branches beyond one level and should only branch in order to make an assertion”(of course, that’s not a very good title though).

If you need to, you can write your own custom assertions with conditional logic inside them to use in your tests. This is allowed because unlike a test an assertion can be tested very easily without your tests becoming absurd.

Just remember to write a test that asserts that your assertion throws the right exceptions and avoid overly complex branching logic inside those assertions.

These are exceptions though, conditional logic should still be treated as a smell, you shouldn’t write a bunch of custom assertions to get around this rule.