Hone logo
Hone
Problems

Replicating Jest's afterEach Hook in TypeScript

Testing frameworks like Jest provide powerful hooks to manage test execution. The afterEach hook is crucial for cleaning up resources or performing actions after every test in a describe block has completed. This challenge asks you to implement a simplified version of Jest's afterEach hook to deepen your understanding of how such hooks operate and how to manage asynchronous operations within them.

Problem Description

Your task is to create a function, let's call it afterEachImpl, that mimics the behavior of Jest's afterEach. This function will accept a callback function as an argument. This callback will be executed after each "test" that is defined.

Key Requirements:

  1. Callback Execution: The provided callback function must be executed after each test.
  2. Asynchronous Support: The callback function might be asynchronous (return a Promise). Your implementation should correctly handle these asynchronous callbacks, ensuring they complete before moving on to the next test.
  3. Global State (Simulated): You'll need to simulate a simple test runner environment. We'll define a describe function and a test function. The afterEachImpl function will be called within the context of a describe block and will register its callback to be invoked after each test call within that describe.

Expected Behavior:

When describe is called with a suite name and a callback, and within that callback test functions are defined, and afterEachImpl is also called with a callback, the afterEachImpl callback should run after each test callback has finished. If the afterEachImpl callback is asynchronous, the next test should not start until the afterEachImpl callback has resolved.

Edge Cases to Consider:

  • What happens if afterEachImpl is called multiple times within a describe block? (For this simplified implementation, assume only one afterEachImpl call per describe block for clarity).
  • What if a test callback throws an error? The afterEachImpl should still execute.

Examples

Let's define some helper functions to simulate the test environment:

// Mock implementation of console.log for testing output
let consoleOutput: string[] = [];
const mockConsoleLog = (message: string) => {
    consoleOutput.push(message);
};

// Reset consoleOutput before each example run
const resetConsoleOutput = () => {
    consoleOutput = [];
};

// Simulated describe and test functions
let afterEachCallback: (() => void | Promise<void>) | null = null;
const describe = (name: string, fn: () => void) => {
    console.log(`Starting describe: ${name}`);
    fn();
    console.log(`Finished describe: ${name}`);
};

const test = async (name: string, fn: () => void | Promise<void>) => {
    console.log(`  Starting test: ${name}`);
    try {
        await fn();
    } catch (error) {
        console.error(`  Test failed: ${name}`, error);
    } finally {
        if (afterEachCallback) {
            console.log(`  Executing afterEach for: ${name}`);
            await afterEachCallback(); // Ensure async afterEach completes
            console.log(`  afterEach finished for: ${name}`);
        }
    }
    console.log(`  Finished test: ${name}`);
};

// The function you need to implement
const afterEachImpl = (callback: () => void | Promise<void>) => {
    afterEachCallback = callback;
};

// --- End of Helper Functions ---

Example 1: Simple Synchronous afterEach

Input (code to run):
resetConsoleOutput();
describe("Math Operations", () => {
    afterEachImpl(() => {
        mockConsoleLog("Cleaning up after a test.");
    });

    test("should add two numbers", () => {
        mockConsoleLog("Running add test.");
        expect(2 + 2).toBe(4);
    });

    test("should subtract two numbers", () => {
        mockConsoleLog("Running subtract test.");
        expect(5 - 3).toBe(2);
    });
});

Output (consoleOutput):
[
    "Starting describe: Math Operations",
    "  Starting test: should add two numbers",
    "Running add test.",
    "  Executing afterEach for: should add two numbers",
    "Cleaning up after a test.",
    "  afterEach finished for: should add two numbers",
    "  Finished test: should add two numbers",
    "  Starting test: should subtract two numbers",
    "Running subtract test.",
    "  Executing afterEach for: should subtract two numbers",
    "Cleaning up after a test.",
    "  afterEach finished for: should subtract two numbers",
    "  Finished test: should subtract two numbers",
    "Finished describe: Math Operations"
]

Example 2: Asynchronous afterEach

Input (code to run):
resetConsoleOutput();
describe("Async Cleanup", () => {
    afterEachImpl(async () => {
        mockConsoleLog("Starting async cleanup...");
        await new Promise(resolve => setTimeout(resolve, 50)); // Simulate async work
        mockConsoleLog("Async cleanup complete.");
    });

    test("should perform an async operation", async () => {
        mockConsoleLog("Running async operation test.");
        await new Promise(resolve => setTimeout(resolve, 20)); // Simulate async work
        mockConsoleLog("Async operation test finished.");
    });

    test("should perform another async operation", async () => {
        mockConsoleLog("Running second async operation test.");
        await new Promise(resolve => setTimeout(resolve, 30)); // Simulate async work
        mockConsoleLog("Second async operation test finished.");
    });
});

Output (consoleOutput - order is critical):
[
    "Starting describe: Async Cleanup",
    "  Starting test: should perform an async operation",
    "Running async operation test.",
    "Async operation test finished.",
    "  Executing afterEach for: should perform an async operation",
    "Starting async cleanup...",
    "Async cleanup complete.",
    "  afterEach finished for: should perform an async operation",
    "  Finished test: should perform an async operation",
    "  Starting test: should perform another async operation",
    "Running second async operation test.",
    "Second async operation test finished.",
    "  Executing afterEach for: should perform another async operation",
    "Starting async cleanup...",
    "Async cleanup complete.",
    "  afterEach finished for: should perform another async operation",
    "  Finished test: should perform another async operation",
    "Finished describe: Async Cleanup"
]

Constraints

  • The afterEachImpl function will only be called once per describe block in your implementation.
  • The test callback might be synchronous or asynchronous (return a Promise).
  • The afterEachImpl callback might be synchronous or asynchronous (return a Promise).
  • The simulated test runner (test function) correctly awaits promises returned by test callbacks.

Notes

  • You are only responsible for implementing the afterEachImpl function. The describe, test, mockConsoleLog, resetConsoleOutput are provided to simulate the environment and check your work.
  • Think about how to store the afterEach callback so that it can be invoked later by the test function.
  • Pay close attention to how you handle the asynchronous nature of the callbacks. The await keyword will be your friend.
  • Consider the lifecycle: describe starts -> test starts -> test finishes -> afterEach runs -> test finishes -> next test starts.
Loading editor...
typescript