Hone logo
Hone
Problems

Mastering Jest Test Fixtures with Data-Driven Testing

This challenge will guide you through creating and utilizing test fixtures in Jest, a powerful technique for managing test data and making your tests more readable, maintainable, and data-driven. Effectively using fixtures allows you to isolate test setup and easily vary inputs for comprehensive testing.

Problem Description

You are tasked with refactoring an existing suite of Jest tests for a simple Calculator class. The current tests are repetitive, with similar data being hardcoded in multiple it blocks. Your goal is to create reusable test fixtures to represent various calculator operations and their expected outcomes. This will make the tests cleaner and easier to extend with new test cases.

Key Requirements:

  1. Create a calculatorFixtures.ts file: This file will house your test fixtures.
  2. Define fixtures for basic arithmetic operations: Include fixtures for addition, subtraction, multiplication, and division.
  3. Each fixture should include:
    • An identifier or name for the test case.
    • The input operands (e.g., a and b for addition).
    • The expected result of the operation.
    • (Optional but recommended) The operation to perform (e.g., 'add', 'subtract').
  4. Refactor existing tests: Replace hardcoded data in your calculator.test.ts file with references to these fixtures.
  5. Utilize test.each: Employ Jest's test.each or it.each to iterate over your fixtures and run a single test for each data set.

Expected Behavior:

Your refactored tests should pass, demonstrating that the Calculator class functions correctly with various inputs. The calculator.test.ts file should be significantly shorter and more readable due to the abstraction of test data.

Edge Cases to Consider:

  • Division by zero.
  • Negative numbers.
  • Floating-point numbers.

Examples

Let's assume a simple Calculator class with methods like add(a: number, b: number): number, subtract(a: number, b: number): number, multiply(a: number, b: number): number, and divide(a: number, b: number): number.

Example 1: Addition Fixture

// calculatorFixtures.ts

interface CalculatorTestCase {
  name: string;
  operation: 'add' | 'subtract' | 'multiply' | 'divide';
  operands: [number, number];
  expected: number | string; // 'Infinity' or specific error message for division by zero
}

export const calculatorFixtures: CalculatorTestCase[] = [
  {
    name: 'should add two positive numbers',
    operation: 'add',
    operands: [5, 3],
    expected: 8,
  },
  // ... other fixtures
];
// calculator.test.ts

import { Calculator } from './calculator'; // Assuming Calculator is in './calculator'
import { calculatorFixtures } from './calculatorFixtures';

describe('Calculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  // Refactored test using test.each
  test.each(calculatorFixtures)('$name', ({ operation, operands, expected }) => {
    let result: number | string;
    const [a, b] = operands;

    switch (operation) {
      case 'add':
        result = calculator.add(a, b);
        break;
      case 'subtract':
        result = calculator.subtract(a, b);
        break;
      case 'multiply':
        result = calculator.multiply(a, b);
        break;
      case 'divide':
        result = calculator.divide(a, b);
        break;
      default:
        throw new Error(`Unknown operation: ${operation}`);
    }

    expect(result).toBe(expected);
  });
});

Example 2: Division by Zero Fixture

// calculatorFixtures.ts

interface CalculatorTestCase {
  name: string;
  operation: 'add' | 'subtract' | 'multiply' | 'divide';
  operands: [number, number];
  expected: number | string;
}

export const calculatorFixtures: CalculatorTestCase[] = [
  // ... other fixtures
  {
    name: 'should handle division by zero',
    operation: 'divide',
    operands: [10, 0],
    expected: Infinity, // Or an expected error message if your divide method throws
  },
];

Example 3: Subtraction with Negative Numbers Fixture

// calculatorFixtures.ts

interface CalculatorTestCase {
  name: string;
  operation: 'add' | 'subtract' | 'multiply' | 'divide';
  operands: [number, number];
  expected: number | string;
}

export const calculatorFixtures: CalculatorTestCase[] = [
  // ... other fixtures
  {
    name: 'should subtract a larger number from a smaller one (resulting in negative)',
    operation: 'subtract',
    operands: [3, 8],
    expected: -5,
  },
];

Constraints

  • Number of Fixtures: You should create at least 10 distinct test cases covering various operations and edge cases.
  • Data Structure: The fixture data should be an array of objects, each representing a single test case with a clear structure.
  • TypeScript: All code, including fixtures and tests, must be written in TypeScript.
  • Jest test.each: The refactored tests must use Jest's test.each or it.each construct.

Notes

  • Consider the return type of your divide method when handling division by zero. Does it return Infinity, NaN, or throw an error? Your fixture's expected value should match this behavior.
  • Think about how you'll pass the operation type to your test loop. A switch statement or an object mapping operation names to functions can be useful.
  • The goal is to decouple your test logic from your test data. Your calculator.test.ts should primarily focus on the testing logic and how it applies to the data provided by the fixtures.
  • For more complex scenarios, you might consider splitting fixtures into different files or using tags to group them, but for this challenge, a single calculatorFixtures.ts file is sufficient.
Loading editor...
typescript