Assertions, such as expect in vitest and jest, are an indispensable part of unit tests. However, assertions inside loops risk being silently skipped. In this post, we discuss some tips and best practices to avoid situations where assertions live in loops.

There is a similar issue for if-clauses discussed in Assertions in If-Clauses in Unit Tests: Tips and Best Practices.

The (Anti-)Pattern

This (anti-)pattern typically looks like:

// test setup...
const array = [...];

array.forEach((element) => {
  // Generate expected...
  expect(element).toBe(expected);
});
// Other assertions...

Or,

// test setup...
const array = [...];

for (const element of array) {
  // Generate expected...
  expect(element).toBe(expected);
});
// Other assertions...

Since the two code snippets above are logically equivalent, we will use the first one throughout the rest of the post.

Solution 1: Assert the Size of the Array

This solution asserts the size of the array to be non-zero before entering the loop to ensure that the loop has been entered at least once, as shown in the highlighted line below:

// test setup...
const array = [...];

expect(array).not.toHaveLength(0);
array.forEach((element) => {
  // Generate expected...
  expect(element).toBe(expected);
});
// Other assertions...

This solution is straightforward but may be error-prone, since the developer may forget to add the array length assertion every time an assertion takes place in a loop.

Solution 2: Use a Wrapper

Instead of using Array.forEach directly, create a wrapper function forEachAtLeastOnce that ensures the loop has been entered at least once:

// test setup...
const array = [...];

forEachAtLeastOnce(array, (element) => {
  // Generate expected...
  expect(element).toBe(expected);
});
// Other assertions...

Inside the definition of forEachAtLeastOnce, assert that the size of the array is larger than zero:

function forEachAtLeastOnce<T>(
  array: readonly T[],
  callback: Parameters<ReadonlyArray<T>["forEach"]>[0],
): void {
  if (array.length === 0) {
    throw {
      message: "Array.forEach did not iterate at least once.",
      name: "ForEachAtLeastOnceError",
    } as const;
  }
  array.forEach(callback);
}

Or in JavaScript:

function forEachAtLeastOnce(array, callback) {
  if (array.length === 0) {
    throw {
      message: "Array.forEach did not iterate at least once.",
      name: "ForEachAtLeastOnceError",
    };
  }
  array.forEach(callback);
}

While this solution may appear to be more complex than Solution 1 due to the wrapper function, it is less error-prone and more concise in the long run.