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:
- Callback Execution: The provided callback function must be executed after each test.
- 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.
- Global State (Simulated): You'll need to simulate a simple test runner environment. We'll define a
describefunction and atestfunction. TheafterEachImplfunction will be called within the context of adescribeblock and will register its callback to be invoked after eachtestcall within thatdescribe.
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
afterEachImplis called multiple times within adescribeblock? (For this simplified implementation, assume only oneafterEachImplcall perdescribeblock for clarity). - What if a
testcallback throws an error? TheafterEachImplshould 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
afterEachImplfunction will only be called once perdescribeblock in your implementation. - The
testcallback might be synchronous or asynchronous (return a Promise). - The
afterEachImplcallback might be synchronous or asynchronous (return a Promise). - The simulated test runner (
testfunction) correctly awaits promises returned by test callbacks.
Notes
- You are only responsible for implementing the
afterEachImplfunction. Thedescribe,test,mockConsoleLog,resetConsoleOutputare provided to simulate the environment and check your work. - Think about how to store the
afterEachcallback so that it can be invoked later by thetestfunction. - Pay close attention to how you handle the asynchronous nature of the callbacks. The
awaitkeyword will be your friend. - Consider the lifecycle:
describestarts ->teststarts ->testfinishes ->afterEachruns ->testfinishes -> nextteststarts.