Hone logo
Hone
Problems

Mutating Your Tests: Implementing a Basic Stryker for Jest

This challenge asks you to build a simplified version of Stryker, a mutation testing tool, specifically for the Jest testing framework. The goal is to introduce subtle changes (mutations) into your source code and then observe how your existing Jest tests react. This helps you identify weaknesses in your test suite, revealing areas where tests might be passing incorrectly due to insufficient coverage or fragile assertions.

Problem Description

You will create a command-line tool that takes a JavaScript/TypeScript source file and a Jest test file as input. Your tool will:

  1. Mutate the Source Code: Introduce a small, predefined mutation into the source code. For this challenge, we'll focus on a single type of mutation: replacing a > operator with a >= operator in a conditional statement.
  2. Run Jest Tests: Execute the provided Jest tests against the mutated source code.
  3. Report Results: Determine if the tests pass or fail with the mutated code.

Key Requirements:

  • Mutation Type: Implement a single mutation: replace all occurrences of > with >= within conditional expressions (e.g., if (a > b), while (x > 0)).
  • Input: Accept two file paths as arguments: sourceFilePath (the code to mutate) and testFilePath (the Jest test file).
  • Jest Integration: Use a programmatic API or spawn a child process to run Jest.
  • Output:
    • If tests pass with the mutated code: Report that the mutation survived.
    • If tests fail with the mutated code: Report that the mutation was killed.
  • Restoration: Ensure the original source file is restored after the process.

Expected Behavior:

Your tool should simulate the core loop of mutation testing: mutate, test, report. A surviving mutation indicates a potential gap in your test suite's ability to detect that specific change. A killed mutation indicates your tests are robust enough to catch that particular alteration.

Edge Cases to Consider:

  • Files not found.
  • Syntax errors in the source code that prevent parsing or mutation.
  • Jest not being installed or configured correctly.
  • Mutations within comments or string literals should be ignored.

Examples

Example 1: Mutation Killed

Let's assume you have a calculator.ts file:

// calculator.ts
export function isGreaterThan(a: number, b: number): boolean {
  return a > b;
}

And a calculator.test.ts file:

// calculator.test.ts
import { isGreaterThan } from './calculator';

describe('Calculator', () => {
  test('should return true when first number is greater', () => {
    expect(isGreaterThan(5, 3)).toBe(true);
  });

  test('should return false when first number is not greater', () => {
    expect(isGreaterThan(3, 5)).toBe(false);
  });

  test('should return false when numbers are equal', () => {
    expect(isGreaterThan(5, 5)).toBe(false); // This test will kill the mutation
  });
});

Input: sourceFilePath: './calculator.ts', testFilePath: './calculator.test.ts'

Mutation: Replace > with >= in calculator.ts. The mutated calculator.ts would become:

// mutated calculator.ts
export function isGreaterThan(a: number, b: number): boolean {
  return a >= b;
}

Jest Execution: Running Jest with the mutated calculator.ts.

  • expect(isGreaterThan(5, 3)).toBe(true); passes (5 >= 3 is true)
  • expect(isGreaterThan(3, 5)).toBe(false); passes (3 >= 5 is false)
  • expect(isGreaterThan(5, 5)).toBe(false); fails (5 >= 5 is true, but expected false)

Output: Mutation killed. Tests failed.

Example 2: Mutation Survived

Consider the same calculator.ts and calculator.test.ts as Example 1. However, this time, let's imagine a different test suite that doesn't specifically test the equality case.

Assume calculator.test.ts only has:

// calculator.test.ts (simplified for survival)
import { isGreaterThan } from './calculator';

describe('Calculator', () => {
  test('should return true when first number is greater', () => {
    expect(isGreaterThan(5, 3)).toBe(true);
  });

  test('should return false when first number is not greater', () => {
    expect(isGreaterThan(3, 5)).toBe(false);
  });
});

Input: sourceFilePath: './calculator.ts', testFilePath: './calculator.test.ts'

Mutation: Replace > with >= in calculator.ts. The mutated calculator.ts would become:

// mutated calculator.ts
export function isGreaterThan(a: number, b: number): boolean {
  return a >= b;
}

Jest Execution: Running Jest with the mutated calculator.ts.

  • expect(isGreaterThan(5, 3)).toBe(true); passes (5 >= 3 is true)
  • expect(isGreaterThan(3, 5)).toBe(false); passes (3 >= 5 is false)

Output: Mutation survived. Tests passed.

Constraints

  • File Size: Source files will not exceed 100KB.
  • Complexity: The mutation type is limited to a single operator replacement (> to >=). You do not need to handle complex AST transformations.
  • Jest Environment: Assume Jest is installed and configured for the project where the source and test files reside.
  • TypeScript Support: The source code can be JavaScript or TypeScript. Your tool should ideally be able to handle both, but focusing on TS is sufficient. You might need a TS compiler or a library to parse TS.
  • Performance: The primary goal is correctness. Performance is secondary, but avoid excessively inefficient operations (e.g., re-reading files many times).

Notes

  • Parsing the AST: You'll likely need to parse the JavaScript/TypeScript source code into an Abstract Syntax Tree (AST) to accurately find and modify the > operators. Libraries like esprima (for JS) or typescript (for TS) can be helpful.
  • Running Jest Programmatically: Consider using Jest's programmatic API (jest.runCLI()) or spawning a child process (child_process.exec or spawn) to execute Jest.
  • File System Operations: Be mindful of file system operations. You'll need to read the original file, write the mutated version, and then restore the original.
  • Focus on the Core Logic: This challenge is about the mutation and testing loop. You don't need to implement a full-fledged mutation testing framework with multiple mutators, complex reporting, or code coverage integration.
  • Restoring Files: A crucial step is to ensure the original source file is restored to its state before your tool ran, regardless of whether the mutation was killed or survived.
Loading editor...
typescript