Hone logo
Hone
Problems

Ensuring Test Isolation with Jest and TypeScript

Test isolation is crucial for writing reliable and maintainable unit tests. This challenge focuses on implementing test isolation techniques in Jest using TypeScript to prevent tests from unintentionally affecting each other's state and producing flaky results. You'll be working with a module that interacts with a shared resource (a mock database) and ensuring each test operates in a clean, isolated environment.

Problem Description

You are given a module user-service.ts that manages user data. This module interacts with a mock database represented by a simple array. The goal is to write Jest tests for this module that are fully isolated. This means each test should start with a clean database state, and any modifications made during a test should not persist and affect subsequent tests. You need to implement techniques like beforeEach and afterEach hooks, and potentially mock the database itself, to achieve this isolation.

What needs to be achieved:

  • Write Jest tests for the UserService class.
  • Ensure that each test runs in an isolated environment, independent of other tests.
  • The mock database should be reset to its initial state before each test.
  • Demonstrate the use of beforeEach and afterEach hooks to manage the test environment.

Key requirements:

  • Use Jest and TypeScript.
  • The UserService class should have methods for adding users and retrieving users.
  • The mock database should be an array of user objects.
  • Tests should verify the correct behavior of the UserService methods.

Expected behavior:

  • Tests should pass consistently, regardless of the order in which they are run.
  • Modifications to the mock database within one test should not affect other tests.
  • The mock database should be empty before each test begins.

Edge cases to consider:

  • What happens if the database is empty when retrieving a user?
  • How do you handle potential errors during user creation? (For simplicity, error handling can be omitted in this challenge, but consider it for real-world scenarios.)

Examples

Example 1:

Input:
user-service.ts:
class UserService {
  private users: { id: number; name: string }[] = [];

  addUser(name: string): number {
    const newUser = { id: this.users.length + 1, name };
    this.users.push(newUser);
    return newUser.id;
  }

  getUser(id: number): { id: number; name: string } | undefined {
    return this.users.find(user => user.id === id);
  }
}

testFile.ts:
import { UserService } from './user-service';

describe('UserService', () => {
  let userService: UserService;

  beforeEach(() => {
    userService = new UserService();
  });

  it('should add a user', () => {
    const userId = userService.addUser('Alice');
    expect(userId).toBe(1);
    expect(userService.getUser(1)).toEqual({ id: 1, name: 'Alice' });
  });
});

Output: All tests pass, and subsequent tests do not rely on the state created in previous tests.

Explanation: beforeEach ensures a new UserService instance (and therefore a fresh, empty database) is created before each test.

Example 2:

Input:
user-service.ts: (same as above)
testFile.ts:
import { UserService } from './user-service';

describe('UserService', () => {
  let userService: UserService;

  beforeEach(() => {
    userService = new UserService();
  });

  afterEach(() => {
    // Not strictly necessary here, as UserService creates a new array each time.
    // But demonstrates the concept.
    userService.users = [];
  });

  it('should retrieve a user', () => {
    const userId = userService.addUser('Bob');
    const user = userService.getUser(userId);
    expect(user).toEqual({ id: userId, name: 'Bob' });
  });

  it('should return undefined when retrieving a non-existent user', () => {
    const user = userService.getUser(999);
    expect(user).toBeUndefined();
  });
});

Output: All tests pass, and the afterEach hook demonstrates resetting the database (although not strictly required in this simple example).

Explanation: afterEach provides a mechanism to clean up after each test, ensuring a clean state for the next test. While not essential here because UserService creates a new users array each time, it's good practice for more complex scenarios.

Constraints

  • The mock database must be an array of objects with id (number) and name (string) properties.
  • Tests must be written in TypeScript.
  • You must use Jest.
  • The solution should be concise and readable.
  • No external libraries beyond Jest and TypeScript are allowed.

Notes

  • Consider using beforeEach to set up the test environment and afterEach to clean up.
  • Think about how to ensure that each test starts with a clean slate.
  • While mocking the entire UserService is possible, the focus here is on isolating the tests within the class itself using lifecycle hooks.
  • This challenge emphasizes the importance of test isolation for writing reliable and maintainable tests.
Loading editor...
typescript