A simple yet powerful testing framework for Node.js backend applications and libraries
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Low-level classes for advanced usage, testing frameworks, and plugin development.
Test execution engine that manages suites, reporters, and the overall test lifecycle.
/**
* Runner class is used to execute the tests
*/
class Runner {
/** Enable or disable bail mode (stop on first failure) */
bail(toggle?: boolean): this;
/** Register a reporter for test output */
registerReporter(reporter: ReporterContract): void;
/** Get test execution summary */
getSummary(): RunnerSummary;
/** Register callback for suite events */
onSuite(callback: (suite: Suite) => void): void;
/** Add a suite to the runner */
add(suite: Suite): void;
/** Start test execution */
async start(): Promise<void>;
/** Execute all tests */
async exec(): Promise<void>;
/** Finish test execution */
async end(): Promise<void>;
}
interface RunnerSummary {
hasError: boolean;
aggregates: {
passed: number;
failed: number;
skipped: number;
todo: number;
total: number;
};
failureTree: Array<{
name: string;
errors: Array<{ phase: string; error: Error }>;
children: Array<{
name: string;
errors: Array<{ phase: string; error: Error }>;
}>;
}>;
}Usage Examples:
import { Runner, Emitter, Suite } from "@japa/runner/core";
// Create and configure runner
const emitter = new Emitter();
const runner = new Runner(emitter);
// Enable bail mode
runner.bail(true);
// Register custom reporter
runner.registerReporter({
name: "custom",
handler: (runner, emitter) => {
emitter.on("test:end", (payload) => {
console.log(`Test: ${payload.title} - ${payload.hasError ? "FAIL" : "PASS"}`);
});
},
});
// Add test suites
const unitSuite = new Suite("unit", emitter, refiner);
runner.add(unitSuite);
// Execute tests
await runner.start();
await runner.exec();
await runner.end();
// Get results
const summary = runner.getSummary();
console.log(`Tests: ${summary.aggregates.total}, Passed: ${summary.aggregates.passed}`);Individual test instance with enhanced functionality and assertion capabilities.
/**
* Test class represents an individual test and exposes API to tweak its runtime behavior
*/
class Test<TestData = undefined> {
/** Assert the test throws an exception with a certain error message */
throws(message: string | RegExp, errorConstructor?: any): this;
/** Set timeout for this test */
timeout(duration: number): this;
/** Set retry count for this test */
retry(count: number): this;
/** Mark test as todo (not implemented) */
todo(): this;
/** Mark test as skipped */
skip(): this;
/** Pin test (only run this test) */
pin(): this;
/** Add test setup hook */
setup(handler: TestHooksHandler<TestContext>): this;
/** Add test cleanup hook */
cleanup(handler: TestHooksCleanupHandler<TestContext>): this;
/** Execute the test */
run(executor: TestExecutor<TestContext, TestData>, debuggingError: Error): this;
}
type TestHooksHandler<Context> = (context: Context) => void | Promise<void>;
type TestHooksCleanupHandler<Context> = (context: Context) => void | Promise<void>;Usage Examples:
import { Test, TestContext, Emitter, Refiner } from "@japa/runner/core";
// Create test instance
const emitter = new Emitter();
const refiner = new Refiner();
const test = new Test("should validate input", (test) => new TestContext(test), emitter, refiner);
// Configure test
test
.timeout(5000)
.retry(2)
.setup(async (context) => {
context.database = await setupTestDatabase();
})
.cleanup(async (context) => {
await cleanupTestDatabase(context.database);
});
// Test that expects an exception
const errorTest = new Test("should throw error", (test) => new TestContext(test), emitter, refiner);
errorTest
.throws("Invalid input", ValidationError)
.run(async (ctx) => {
throw new ValidationError("Invalid input");
}, new Error());
// Pin test for focused debugging
const debugTest = new Test("debug test", (test) => new TestContext(test), emitter, refiner);
debugTest
.pin()
.run(async (ctx) => {
// This test will run exclusively when pinned
// Note: You would need to import assert from your assertion library
// assert.isTrue(true);
}, new Error());Test context that carries data and provides utilities for individual tests.
/**
* Test context carries context data for a given test
*/
class TestContext {
/** The test instance this context belongs to */
test: Test;
/** Register a cleanup function that runs after the test finishes */
cleanup(cleanupCallback: TestHooksCleanupHandler<TestContext>): void;
constructor(test: Test);
}Usage Examples:
import { test } from "@japa/runner";
test("should handle cleanup", async (ctx) => {
// Set up resources
const connection = await createDatabaseConnection();
const tempFile = await createTempFile();
// Register cleanup functions
ctx.cleanup(async () => {
await connection.close();
console.log("Database connection closed");
});
ctx.cleanup(async () => {
await deleteTempFile(tempFile);
console.log("Temp file deleted");
});
// Test logic
await connection.query("SELECT 1");
await writeToFile(tempFile, "test data");
// Cleanup functions will run automatically after test completion
});Container for organizing related tests with shared configuration and hooks.
/**
* TestGroup is used to bulk configure a collection of tests and define lifecycle hooks
*/
class Group {
/** Add a test to this group */
add(test: Test): void;
/** Enable bail mode for this group */
bail(toggle?: boolean): this;
/** Set timeout for all tests in group */
timeout(duration: number): this;
/** Set retry count for all tests in group */
retry(count: number): this;
/** Add group setup hook (runs once before all tests) */
setup(handler: GroupHooksHandler<TestContext>): this;
/** Add group teardown hook (runs once after all tests) */
teardown(handler: GroupHooksHandler<TestContext>): this;
/** Add hooks that run before/after each test */
each: {
setup(handler: GroupHooksHandler<TestContext>): Group;
teardown(handler: GroupHooksHandler<TestContext>): Group;
};
/** Apply function to all tests in group */
tap(handler: (test: Test) => void): this;
}
type GroupHooksHandler<Context> = (context: Context) => void | Promise<void>;Usage Examples:
import { Group, Suite, Emitter, Refiner } from "@japa/runner/core";
// Create group
const emitter = new Emitter();
const refiner = new Refiner();
const group = new Group("Database Tests", emitter, refiner);
// Configure group
group
.timeout(10000)
.retry(1)
.bail(false);
// Add group-level hooks
group.setup(async (context) => {
console.log("Setting up database for all tests");
context.db = await setupDatabase();
});
group.teardown(async (context) => {
console.log("Cleaning up database after all tests");
await cleanupDatabase(context.db);
});
// Add per-test hooks
group.each.setup(async (context) => {
await context.db.beginTransaction();
});
group.each.teardown(async (context) => {
await context.db.rollbackTransaction();
});
// Apply configuration to all tests
group.tap((test) => {
test.timeout(5000);
});Top-level container representing a collection of tests and groups for a specific testing category.
/**
* A suite is a collection of tests created around a given testing type
*/
class Suite {
/** The suite name */
name: string;
/** Add a test or group to this suite */
add(testOrGroup: Test | Group): void;
/** Enable bail mode for this suite */
bail(toggle?: boolean): this;
/** Set timeout for all tests in suite */
timeout(duration: number): this;
/** Set retry count for all tests in suite */
retry(count: number): this;
/** Register callback for group events */
onGroup(callback: (group: Group) => void): void;
/** Register callback for test events */
onTest(callback: (test: Test) => void): void;
}Usage Examples:
import { Suite, Group, Test, Emitter, Refiner } from "@japa/runner/core";
// Create suite
const emitter = new Emitter();
const refiner = new Refiner();
const suite = new Suite("Integration Tests", emitter, refiner);
// Configure suite
suite
.timeout(30000)
.bail(true);
// Listen to events
suite.onGroup((group) => {
console.log(`Group added to suite: ${group.title}`);
});
suite.onTest((test) => {
console.log(`Test added to suite: ${test.title}`);
});
// Add tests and groups
const apiGroup = new Group("API Tests", emitter, refiner);
suite.add(apiGroup);
const standaloneTest = new Test("standalone test", (test) => new TestContext(test), emitter, refiner);
suite.add(standaloneTest);Event emitter for communication between test runner components.
/**
* Event emitter for test runner communication
*/
class Emitter {
/** Listen to an event */
on(event: string, callback: (...args: any[]) => void): void;
/** Emit an event */
emit(event: string, ...args: any[]): void;
/** Listen to an event once */
once(event: string, callback: (...args: any[]) => void): void;
/** Remove event listener */
off(event: string, callback: (...args: any[]) => void): void;
}Usage Examples:
import { Emitter } from "@japa/runner/core";
const emitter = new Emitter();
// Listen to events
emitter.on("test:start", (payload) => {
console.log(`Test started: ${payload.title}`);
});
emitter.on("test:end", (payload) => {
const status = payload.hasError ? "FAILED" : "PASSED";
console.log(`Test ${status}: ${payload.title}`);
});
// Emit events
emitter.emit("test:start", { title: "My Test" });
emitter.emit("test:end", { title: "My Test", hasError: false });
// One-time listener
emitter.once("runner:end", () => {
console.log("All tests completed");
});interface TestExecutor<Context, TestData> {
(context: Context, done?: Function): void | Promise<void>;
}
interface ReporterContract {
name: string;
handler: (runner: Runner, emitter: Emitter) => void;
}
interface TestHooksHandler<Context> {
(context: Context): void | Promise<void>;
}
interface TestHooksCleanupHandler<Context> {
(context: Context): void | Promise<void>;
}
interface GroupHooksHandler<Context> {
(context: Context): void | Promise<void>;
}interface RunnerSummary {
hasError: boolean;
aggregates: {
passed: number;
failed: number;
skipped: number;
todo: number;
total: number;
};
failureTree: Array<{
name: string;
errors: Array<{ phase: string; error: Error }>;
children: Array<{
name: string;
errors: Array<{ phase: string; error: Error }>;
}>;
}>;
}