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();
});