Hone logo
Hone
Problems

Jest Test Isolation: The Unshared State Challenge

In software development, ensuring that tests don't interfere with each other is paramount for reliable and maintainable codebases. When tests share state, the order in which they run can drastically impact their outcome, leading to flaky tests and difficult debugging. This challenge focuses on implementing effective test isolation in Jest.

Problem Description

Your task is to write a set of Jest tests for a simple module that manages a counter. This module has functions to increment, decrement, and reset the counter. The primary goal is to ensure that each test runs in isolation, meaning the state of the counter from one test does not affect the state in another. You will be given a starter module with potential state-sharing issues and tasked with refactoring the tests to achieve true isolation.

What needs to be achieved:

  • Write Jest tests for the provided Counter module.
  • Ensure that each test starts with a fresh, predictable state for the counter.
  • Demonstrate how to prevent tests from interfering with each other's state.

Key requirements:

  • Use Jest for testing.
  • Implement test isolation using appropriate Jest features.
  • The Counter module should function as expected:
    • increment(): Increases the counter by 1.
    • decrement(): Decreases the counter by 1.
    • reset(): Sets the counter back to 0.
    • getValue(): Returns the current counter value.

Expected behavior:

  • Each test case should independently verify the functionality of the Counter module.
  • For instance, a test that increments the counter should not cause a subsequent test that expects the counter to be 0 (after a reset) to fail.

Edge cases to consider:

  • Decrementing the counter when its value is already 0. (The counter should not go below zero in this implementation).

Examples

Let's assume the Counter module is defined as follows (you will be provided with this code to test):

// counter.ts
export class Counter {
  private count: number = 0;

  increment(): void {
    this.count++;
  }

  decrement(): void {
    if (this.count > 0) {
      this.count--;
    }
  }

  reset(): void {
    this.count = 0;
  }

  getValue(): number {
    return this.count;
  }
}

Example 1: Basic Increment Test

Input (to the test execution):

// counter.test.ts
import { Counter } from './counter';

describe('Counter Module', () => {
  it('should increment the counter correctly', () => {
    const counter = new Counter();
    counter.increment();
    expect(counter.getValue()).toBe(1);
  });
});

Output (from Jest, assuming a clean start for the Counter instance within this test):

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

Example 2: Reset Test

Input (to the test execution):

// counter.test.ts
import { Counter } from './counter';

describe('Counter Module', () => {
  it('should reset the counter to zero', () => {
    const counter = new Counter();
    counter.increment();
    counter.increment();
    counter.reset();
    expect(counter.getValue()).toBe(0);
  });
});

Output (from Jest, assuming a clean start for the Counter instance within this test):

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

Example 3: Preventing State Leakage (Illustrative Scenario)

Imagine a test file with the following tests, without proper isolation:

// counter.test.ts (BAD EXAMPLE - demonstrates state leakage)
import { Counter } from './counter';

const counter = new Counter(); // Shared instance

describe('Counter Module', () => {
  it('should increment the counter', () => {
    counter.increment();
    expect(counter.getValue()).toBe(1); // This might pass if run first
  });

  it('should be zero after reset', () => {
    counter.reset();
    expect(counter.getValue()).toBe(0); // This might FAIL if the previous test ran and didn't reset!
  });
});

The goal of this challenge is to write counter.test.ts correctly so that the Counter instance is managed such that the second test always passes, regardless of the first test's execution.

Constraints

  • Your solution must be written in TypeScript.
  • You must use Jest as your testing framework.
  • The Counter class provided in the examples is the target for your tests. You do not need to modify this class, only test it.
  • All tests must pass when run using jest.
  • The state of the Counter should be reset for each individual test case.

Notes

  • Consider how Jest provides mechanisms to set up and tear down state before and after tests or test suites.
  • Think about the scope of your Counter instance within your test file. Is it declared globally for the file, or within each test?
  • The beforeEach and afterEach hooks in Jest are particularly relevant for managing test isolation.
  • Ensure you have Jest and its TypeScript types installed in your project (npm install --save-dev jest @types/jest ts-jest typescript). You'll also need a jest.config.js file configured for TypeScript.
Loading editor...
typescript