Hone logo
Hone
Problems

Mastering Parameterized Tests with Jest's test.each

As developers, writing robust and comprehensive tests is crucial for ensuring the quality and reliability of our code. Often, we need to test a single function or piece of logic with a variety of inputs and expected outputs. Manually writing separate test blocks for each scenario can become repetitive and lead to verbose test suites. Jest's test.each provides an elegant solution for this by allowing us to define parameterized tests, making our tests more concise and maintainable.

This challenge will guide you through implementing and understanding test.each in Jest using TypeScript. You will refactor existing tests into parameterized tests and create new ones from scratch, solidifying your grasp of this powerful testing feature.

Problem Description

Your task is to refactor a given set of Jest tests for a simple arithmetic function into a more efficient format using test.each. You will also be required to implement new parameterized tests for a slightly more complex scenario.

What needs to be achieved:

  1. Refactor existing tests: Take a series of individual test blocks that all test the same function with different inputs and consolidate them into a single test.each block.
  2. Implement new parameterized tests: Create test.each blocks for a new function that handles string manipulation.
  3. Ensure clarity and readability: The refactored and new tests should be easy to understand and maintain.

Key requirements:

  • Use test.each with an array of arrays or an array of objects.
  • The tests should cover various valid inputs and at least one edge case.
  • All tests must be written in TypeScript.
  • The original function logic should remain unchanged.

Expected behavior:

  • All tests should pass after refactoring and implementation.
  • The test suite should be significantly more concise than the original version.
  • The test descriptions should clearly indicate which input is being tested.

Edge cases to consider:

  • Zero
  • Negative numbers
  • Empty strings
  • Strings with only whitespace

Examples

Let's assume we have a simple add function:

// src/math.ts
export function add(a: number, b: number): number {
  return a + b;
}

Example 1: Refactoring the add function

Original Tests (before refactoring):

// src/math.test.ts
import { add } from './math';

describe('add function', () => {
  test('should add two positive numbers', () => {
    expect(add(5, 3)).toBe(8);
  });

  test('should add a positive and a negative number', () => {
    expect(add(5, -3)).toBe(2);
  });

  test('should add two negative numbers', () => {
    expect(add(-5, -3)).toBe(-8);
  });

  test('should add zero to a number', () => {
    expect(add(0, 7)).toBe(7);
    expect(add(7, 0)).toBe(7);
  });
});

Refactored Tests (using test.each with an array of arrays):

Input: The add function and a set of input-output pairs.

Output: A single test.each block that covers all scenarios.

// src/math.test.ts (refactored)
import { add } from './math';

describe('add function with test.each', () => {
  test.each([
    [5, 3, 8],
    [5, -3, 2],
    [-5, -3, -8],
    [0, 7, 7],
    [7, 0, 7],
    [100, 200, 300],
  ])('adds %p and %p to equal %p', (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

Explanation: The individual test blocks are replaced by a single test.each block. Each inner array represents a test case, with the values being passed as arguments to the test callback function. The %p placeholders in the test description are replaced with the corresponding arguments from each test case, making the output descriptive.

Example 2: Implementing test.each with an array of objects

Let's consider a new function reverseString:

// src/stringUtils.ts
export function reverseString(str: string): string {
  return str.split('').reverse().join('');
}

Input: The reverseString function and various string inputs.

Output: test.each block using an array of objects to test the reverseString function.

// src/stringUtils.test.ts
import { reverseString } from './stringUtils';

describe('reverseString function with test.each (objects)', () => {
  test.each([
    { input: 'hello', expected: 'olleh', description: 'should reverse a simple string' },
    { input: 'Jest', expected: 'tseJ', description: 'should reverse a string with uppercase letters' },
    { input: '', expected: '', description: 'should return an empty string for empty input' },
    { input: 'a', expected: 'a', description: 'should return the same string for a single character' },
    { input: '  spaces  ', expected: '  secaps  ', description: 'should handle leading/trailing spaces' },
  ])('$description: reversing "$input" should be "$expected"', ({ input, expected, description }) => {
    expect(reverseString(input)).toBe(expected);
  });
});

Explanation: This example demonstrates using test.each with an array of objects. Each object defines a test case with named properties like input, expected, and description. The test description uses template literals to dynamically include the input, expected output, and the custom description for each test case, improving readability.

Constraints

  • Your solution must be written in TypeScript.
  • You must use test.each for all tests related to the provided functions.
  • The tests must run successfully with Jest.
  • Avoid hardcoding specific values directly within the expect calls; all test data should be part of the test.each argument.

Notes

  • test.each supports two primary ways of defining test cases: an array of arrays and an array of objects. Familiarize yourself with both.
  • Pay close attention to the syntax for interpolating values into test descriptions (e.g., %p, %s, or template literals with object properties).
  • When using an array of objects, the keys of the objects will be available as properties within the test callback function, making tests more self-documenting.
  • Consider how test.each can help you test edge cases more systematically.
Loading editor...
typescript