Assertions, such as expect
in vitest and jest, are an indispensable part of unit tests.
However, assertions inside if
-clauses risk being silently skipped. There is even an ESLint rule
no-conditional-expect
that checks conditional
assertions.
In this post, we discuss some tips and best practices to avoid situations where assertions live in
if
-clauses.
There is a similar issue for loops discussed in Assertions in Loops in Unit Tests: Tips and Best Practices.
The (Anti-)Pattern
Usually found in parameterized tests, this (anti-)pattern typically looks like:
// test setup...
if (condition) {
expect(actual).toBe(expected);
}
// Other assertions...
There are two possible cases:
condition
is directly determined by the test output. For example, if an HTML input element is shown (condition
), then its content (actual
) is expected to be a specific value (expected
).condition
is directly determined by the test input. For example, if the input is not null (condition
), then the output (actual
) is expected to be a specific value (expected
).
condition
is Directly Determined by Output
In this case, unless there’s a special justifying circumstance, it’s better also to test the condition itself:
// test setup...
expect(condition).toBeTruthy();
expect(actual).toBe(expected);
// Other assertions...
condition
is Directly Determined by Input
In this case, it’s worth considering creating separate tests for condition
:
test("condition is true", () => {
// test setup...
expect(actual).toBe(expected);
// Other assertions...
});
test("condition is false", () => {
// test setup...
// Other assertions...
});
When Multiple Such if
-Clauses Contain Assertions
If condition
is directly determined by input and there are multiple such if
-clauses that contain
assertions in a test, following the recommendation above may result in too many tests, since we may
need a test for each combination. For example, a test with 5 such if
-clauses will turn into
25 = 32 tests!
In this case, chances are that we are testing many behaviors in a single test. We can address this by splitting each behavior into its own test.
Example
Let’s say for example, that we are testing a clock app/program. An “AM” or “PM” is shown depending on the time of the day. If the time is exactly on an hour, such as 1 o’clock, the clock makes a sound.
A test may look like:
const testTimes = ["01:00:00", "12:01:10"];
for (const testTime of testTimes) {
test(`Test the behavior of the clock at time ${testTime}`, () => {
setClockTime(testTime);
if (isBeforeMorning(testTime)) {
expect(isAMShown()).toBeTruthy();
} else {
expect(isPMShown()).toBeTruthy();
}
if (isOnAnHour(testTime)) {
expect(madeSound()).toBeTruthy();
}
});
}
This test consists of multiple if
-clauses that contain assertions. The reason is that the test is
testing too many behaviors at once. We can rewrite this test into multiple tests, each of which
tests only one behavior:
test("In the morning, shows AM", () => {
setClockTime("01:00:00");
expect(isAMShown()).toBeTruthy();
});
test("In the afternoon, shows PM", () => {
setClockTime("12:01:10");
expect(isPMShown()).toBeTruthy();
});
test("On an hour, makes a sound", () => {
setClockTime("01:00:00");
expect(madeSound()).toBeTruthy();
});