Custom Reporter
How to create a custom reporter for Testurio. Reporters receive lifecycle events during test execution and can generate reports in any format.
IReporter Interface
interface IReporter {
readonly name: string;
onStart?(result: { name?: string; startTime: number }): void;
onTestCaseStart?(testCase: { name: string }): void;
onStepComplete?(step: TestStepResult): void;
onTestCaseComplete?(result: TestCaseResult): void;
onComplete(result: TestResult): void;
onError?(error: Error): void;
}All methods except onComplete and name are optional. Implement only the events you need.
Lifecycle Events
| Event | When | Data |
|---|---|---|
onStart | Scenario begins | Scenario name, start time |
onTestCaseStart | Test case begins | Test case name |
onStepComplete | Each step finishes | Step result (name, status, duration, error, metadata) |
onTestCaseComplete | Test case finishes | Full test case result with all steps |
onComplete | Scenario ends | Full test result with all test cases |
onError | Unhandled error | Error object |
Per-Step Payloads (TestStepResult.metadata)
Components stamp request/response payloads on step.metadata during execution; the runtime propagates them to TestStepResult.metadata. Custom reporters can read these directly without enabling TestScenario({ recording: true }):
onStepComplete(step: TestStepResult): void {
const meta = step.metadata;
if (meta?.request !== undefined) {
console.log(`Step ${step.stepNumber} request:`, meta.request);
}
if (meta?.response !== undefined) {
console.log(`Step ${step.stepNumber} response:`, meta.response);
}
}Recognized payload keys (stamped by built-in components): request, response, message, payload, data, body. The set of keys present depends on the component type and step type — see the Reporting guide for the full per-component contract.
Basic Example: Console Reporter
import type { IReporter, TestResult, TestCaseResult, TestStepResult } from 'testurio';
class ConsoleReporter implements IReporter {
readonly name = 'console';
onStart(result: { name?: string; startTime: number }): void {
console.log(`\nRunning: ${result.name ?? 'unnamed'}`);
console.log('─'.repeat(40));
}
onTestCaseStart(testCase: { name: string }): void {
console.log(` Test: ${testCase.name}`);
}
onStepComplete(step: TestStepResult): void {
const icon = step.passed ? ' ✓' : ' ✗';
console.log(` ${icon} ${step.name} (${step.duration}ms)`);
}
onTestCaseComplete(result: TestCaseResult): void {
const icon = result.passed ? '✓' : '✗';
console.log(` ${icon} ${result.name} — ${result.duration}ms`);
}
onComplete(result: TestResult): void {
console.log('─'.repeat(40));
const total = result.testCases.length;
const passed = result.testCases.filter((tc) => tc.passed).length;
console.log(`Result: ${passed}/${total} passed (${result.duration}ms)\n`);
}
onError(error: Error): void {
console.error(`Error: ${error.message}`);
}
}Advanced Example: JSON File Reporter
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { IReporter, TestResult } from 'testurio';
class JsonReporter implements IReporter {
readonly name = 'json';
constructor(private outputDir: string) {
fs.mkdirSync(outputDir, { recursive: true });
}
onComplete(result: TestResult): void {
const filename = `report-${Date.now()}.json`;
const filepath = path.join(this.outputDir, filename);
const report = {
name: result.name,
passed: result.passed,
duration: result.duration,
timestamp: new Date().toISOString(),
testCases: result.testCases.map((tc) => ({
name: tc.name,
passed: tc.passed,
duration: tc.duration,
steps: tc.steps.map((step) => ({
name: step.name,
passed: step.passed,
duration: step.duration,
error: step.error?.message,
})),
})),
};
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
}
}Using a Custom Reporter
import { TestScenario } from 'testurio';
const scenario = new TestScenario({
name: 'My Tests',
components: [server, client],
reporters: [
new ConsoleReporter(),
new JsonReporter('./test-reports'),
],
});Multiple reporters can be used simultaneously. Each receives all events.
Packaging
Package as @testurio/reporter-* or testurio-reporter-*:
{
"peerDependencies": {
"testurio": "^0.x"
}
}