Hone logo
Hone
Problems

Implementing a Jest Mutation Score Calculator

This challenge focuses on understanding and simulating the concept of mutation testing within a Jest testing environment. Mutation testing is a technique used to evaluate the quality of your test suite. It works by making small, deliberate changes (mutations) to your source code and then checking if your existing tests fail. A high mutation score indicates a robust test suite.

Problem Description

Your task is to create a TypeScript function that simulates the process of calculating a mutation score. You will be provided with an array of "mutated" code snippets (represented as strings) and a Jest test suite (also represented as a string). Your function needs to execute these tests against each mutated code snippet and determine how many mutations were "killed" (i.e., caused a test to fail). The mutation score will then be calculated based on the number of killed mutations versus the total number of mutations.

Key Requirements:

  • Simulate Mutation Execution: You'll need a mechanism to dynamically execute Jest tests against different versions of source code. For this challenge, we will simulate this by having a predefined set of "mutated" code strings.
  • Track Test Outcomes: For each mutated code string, you must determine if the provided Jest tests pass or fail.
  • Calculate Mutation Score: The mutation score is calculated as: (Number of Killed Mutations / Total Number of Mutations) * 100.
  • Handle No Mutations: If there are no mutations provided, the score should be 100% (as there are no mutations to kill).

Expected Behavior:

Your function should accept two arguments:

  1. sourceCode: A string representing the original source code.
  2. mutatedCodes: An array of strings, where each string is a mutated version of the sourceCode.
  3. jestTestCode: A string representing the Jest test suite to be run against the source code.

The function should return a number representing the mutation score as a percentage.

Edge Cases:

  • No mutatedCodes: The function should handle this gracefully and return 100%.
  • Tests that always pass/fail: The simulation should correctly identify if a mutation is killed or not, regardless of the inherent nature of the tests.

Examples

Example 1:

Input:

sourceCode:

export function add(a: number, b: number): number {
  return a + b;
}

mutatedCodes:

[
  "export function add(a: number, b: number): number { return a - b; }", // Mutation 1: Changed '+' to '-'
  "export function add(a: number, b: number): number { return a + b + 1; }" // Mutation 2: Added '+ 1'
]

jestTestCode:

import { add } from './source'; // Assume sourceCode is saved as ./source.ts

describe('add function', () => {
  test('should return the sum of two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  test('should handle negative numbers', () => {
    expect(add(-1, 5)).toBe(4);
  });
});

Output: 50

Explanation:

  • Mutation 1 (a - b): When the original add function is mutated to a - b, the test expect(add(2, 3)).toBe(5) will fail because 2 - 3 is -1, not 5. This mutation is killed.
  • Mutation 2 (a + b + 1): When the original add function is mutated to a + b + 1, the test expect(add(2, 3)).toBe(5) will fail because 2 + 3 + 1 is 6, not 5. This mutation is killed.
  • Total Mutations: 2
  • Killed Mutations: 2
  • Mutation Score: (2 / 2) * 100 = 100. Correction: My initial explanation for Example 1 was incorrect. Let's re-evaluate.

Example 1 (Corrected Explanation):

Input: (Same as above)

sourceCode:

export function add(a: number, b: number): number {
  return a + b;
}

mutatedCodes:

[
  "export function add(a: number, b: number): number { return a - b; }", // Mutation 1: Changed '+' to '-'
  "export function add(a: number, b: number): number { return a + b + 1; }" // Mutation 2: Added '+ 1'
]

jestTestCode:

import { add } from './source'; // Assume sourceCode is saved as ./source.ts

describe('add function', () => {
  test('should return the sum of two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  test('should handle negative numbers', () => {
    expect(add(-1, 5)).toBe(4);
  });
});

Output: 50

Explanation:

  • Mutation 1 (a - b): When add(a, b) becomes a - b, the test expect(add(2, 3)).toBe(5) fails because 2 - 3 is -1. The mutation is killed.
  • Mutation 2 (a + b + 1): When add(a, b) becomes a + b + 1, the test expect(add(2, 3)).toBe(5) fails because 2 + 3 + 1 is 6. The mutation is killed.
  • Wait, there's a misunderstanding of the problem. The challenge is to simulate the process, not to actually run Jest in a sandbox. The example output should reflect the expected outcome of such a simulation.

Let's reframe Example 1 to better illustrate the simulation concept:

Example 1 (Revised for Simulation Context):

Input:

sourceCode:

export function multiply(a: number, b: number): number {
  return a * b;
}

mutatedCodes:

[
  "export function multiply(a: number, b: number): number { return a / b; }", // Mutation 1: Changed '*' to '/'
  "export function multiply(a: number, b: number): number { return a + b; }"  // Mutation 2: Changed '*' to '+'
]

jestTestCode:

// This is conceptual. In a real scenario, Jest would execute this.
// For this simulation, we'll assume the outcomes are pre-determined or we'd need a way to run Jest dynamically.
// For the purpose of this challenge, we'll assume 'killed' means the mutation *would* cause a test to fail.
// We'll rely on our understanding of the code to determine 'killed'.

/*
describe('multiply function', () => {
  test('should return the product of two numbers', () => {
    expect(multiply(2, 3)).toBe(6); // Passes with original code
  });
  test('should handle zero', () => {
    expect(multiply(5, 0)).toBe(0); // Passes with original code
  });
});
*/

Expected Outcome of Simulation:

  • Mutation 1 (a / b):
    • Test expect(multiply(2, 3)).toBe(6): 2 / 3 is 0.66..., which is NOT 6. This test would fail. Mutation killed.
    • Test expect(multiply(5, 0)).toBe(0): 5 / 0 would result in Infinity or an error, which is NOT 0. This test would fail. Mutation killed.
  • Mutation 2 (a + b):
    • Test expect(multiply(2, 3)).toBe(6): 2 + 3 is 5, which is NOT 6. This test would fail. Mutation killed.
    • Test expect(multiply(5, 0)).toBe(0): 5 + 0 is 5, which is NOT 0. This test would fail. Mutation killed.

Output: 100

Explanation: Both mutations cause at least one test to fail. Therefore, both mutations are killed. The mutation score is (2 killed / 2 total) * 100 = 100.

Example 2:

Input:

sourceCode:

export function isEven(n: number): boolean {
  return n % 2 === 0;
}

mutatedCodes:

[
  "export function isEven(n: number): boolean { return n % 2 !== 0; }" // Mutation 1: Changed '===' to '!=='
]

jestTestCode:

/*
import { isEven } from './source';

describe('isEven function', () => {
  test('should return true for even numbers', () => {
    expect(isEven(4)).toBe(true); // Passes with original code
  });
  test('should return false for odd numbers', () => {
    expect(isEven(3)).toBe(false); // Passes with original code
  });
});
*/

Expected Outcome of Simulation:

  • Mutation 1 (n % 2 !== 0):
    • Test expect(isEven(4)).toBe(true): 4 % 2 !== 0 is 0 !== 0, which is false. The expected result was true. This test would fail. Mutation killed.
    • Test expect(isEven(3)).toBe(false): 3 % 2 !== 0 is 1 !== 0, which is true. The expected result was false. This test would fail. Mutation killed.

Output: 100

Explanation: The single mutation causes both tests to fail. Therefore, the mutation is killed. The mutation score is (1 killed / 1 total) * 100 = 100.

Example 3 (Edge Case: No Mutations):

Input:

sourceCode:

export function greet(name: string): string {
  return `Hello, ${name}!`;
}

mutatedCodes:

[]

jestTestCode:

/*
import { greet } from './source';

describe('greet function', () => {
  test('should greet a person', () => {
    expect(greet('World')).toBe('Hello, World!');
  });
});
*/

Output: 100

Explanation: When there are no mutations, the mutation score is considered 100% because there are no mutations to kill.

Constraints

  • The sourceCode and each string in mutatedCodes will be valid TypeScript code that can be compiled and potentially executed.
  • The jestTestCode will be valid Jest syntax.
  • For the purpose of this challenge, you are not expected to implement a full Jest execution environment. You will need to simulate the outcome of running the tests against the mutated code. This means you will need to analyze the code yourself and determine if a mutation is "killed" based on the provided tests.
  • The number of mutatedCodes will not exceed 100.

Notes

This challenge is designed to test your understanding of code logic and how test cases interact with code changes. You'll need to write a function that iterates through the mutatedCodes, analyzes each one in conjunction with the jestTestCode, and calculates the score. Think about how you would programmatically determine if a test fails given a specific code change.

The core of this challenge lies in the simulation logic you implement to decide if a mutation is "killed." You will need to apply your understanding of programming and testing to determine this outcome.

Loading editor...
typescript