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.