A Test-Anything-Protocol library for JavaScript with comprehensive testing capabilities and plugin-based architecture
—
Test external processes, worker threads, and stdin/stdout interactions for comprehensive integration testing.
Test external commands and processes with full control over arguments, environment, and stdio.
/**
* Spawn a subprocess for testing with command and arguments
* @param cmd - Command to execute
* @param args - Optional array of command arguments
* @param options - Optional spawn configuration
* @param name - Optional test name override
* @returns Promise that resolves when subprocess test completes
*/
function spawn(cmd: string, args?: string[], options?: SpawnOpts, name?: string): Promise<void>;
/**
* Spawn a subprocess with just command (arguments parsed from command string)
* @param cmd - Command with arguments as single string
* @param options - Optional spawn configuration
* @param name - Optional test name override
* @returns Promise that resolves when subprocess test completes
*/
function spawn(cmd: string, options?: SpawnOpts, name?: string): Promise<void>;Usage Examples:
import { test, spawn } from "tap";
// Test CLI command
test("CLI command execution", async (t) => {
await spawn("echo", ["hello world"], {
expect: {
code: 0,
stdout: "hello world\n"
}
});
});
// Test with options
test("node script execution", async (t) => {
await spawn("node", ["-e", "console.log('test')"], {
expect: {
code: 0,
stdout: /test/
},
timeout: 5000
});
});
// Test command failure
test("failing command", async (t) => {
await spawn("node", ["-e", "process.exit(1)"], {
expect: {
code: 1
}
});
});Test Node.js worker threads for parallel processing and isolation.
/**
* Create and test a worker thread
* @param file - Path to worker script file
* @param options - Optional worker configuration
* @param name - Optional test name override
* @returns Promise that resolves when worker test completes
*/
function worker(file: string, options?: WorkerOpts, name?: string): Promise<void>;Usage Examples:
// Test worker thread
test("worker thread execution", async (t) => {
await worker("./test-worker.js", {
expect: {
code: 0,
stdout: "worker completed\n"
},
workerData: { input: "test data" }
});
});
// Test worker with data exchange
test("worker data exchange", async (t) => {
await worker("./calculator-worker.js", {
expect: {
stdout: "42\n"
},
workerData: { operation: "add", a: 20, b: 22 }
});
});Test programs that read from standard input.
/**
* Test a program with stdin input
* @param input - String or buffer to send to stdin
* @param options - Optional stdin test configuration
* @param name - Optional test name override
* @returns Promise that resolves when stdin test completes
*/
function stdin(input: string | Buffer, options?: StdinOpts, name?: string): Promise<void>;
/**
* Test with stdin only (no command specified, tests current process)
* @param input - String or buffer to send to stdin
* @param options - Optional configuration
* @param name - Optional test name override
* @returns Promise that resolves when test completes
*/
function stdinOnly(input: string | Buffer, options?: StdinOpts, name?: string): Promise<void>;Usage Examples:
// Test program that reads stdin
test("stdin processing", async (t) => {
await spawn("node", ["./stdin-processor.js"], {
stdin: "hello\nworld\n",
expect: {
stdout: "HELLO\nWORLD\n"
}
});
});
// Test with stdin helper
test("direct stdin test", async (t) => {
await stdin("test input\n", {
command: ["node", "./echo-server.js"],
expect: {
stdout: "received: test input"
}
});
});Comprehensive options for controlling subprocess behavior.
interface SpawnOpts {
/** Expected exit conditions */
expect?: {
/** Expected exit code */
code?: number;
/** Expected stdout content (string or regex) */
stdout?: string | RegExp;
/** Expected stderr content (string or regex) */
stderr?: string | RegExp;
/** Signal that should terminate the process */
signal?: string;
};
/** Timeout in milliseconds */
timeout?: number;
/** Environment variables */
env?: { [key: string]: string };
/** Working directory */
cwd?: string;
/** Input to send to stdin */
stdin?: string | Buffer;
/** Encoding for stdout/stderr */
encoding?: string;
/** Whether to capture stdout */
stdout?: boolean;
/** Whether to capture stderr */
stderr?: boolean;
}
interface WorkerOpts extends SpawnOpts {
/** Data to pass to worker thread */
workerData?: any;
/** Resource limits for worker */
resourceLimits?: {
maxOldGenerationSizeMb?: number;
maxYoungGenerationSizeMb?: number;
codeRangeSizeMb?: number;
};
}
interface StdinOpts extends SpawnOpts {
/** Command to run (if not using stdinOnly) */
command?: string[];
}Complex testing scenarios with multiple processes and interactions.
// Test process communication
test("process pipe communication", async (t) => {
// Test producer -> consumer pipeline
await spawn("echo", ["data"], {
pipe: {
to: ["node", "./data-processor.js"]
},
expect: {
code: 0,
stdout: "processed: data"
}
});
});
// Test with environment variables
test("environment-dependent process", async (t) => {
await spawn("node", ["-e", "console.log(process.env.TEST_VAR)"], {
env: {
TEST_VAR: "test-value"
},
expect: {
stdout: "test-value\n"
}
});
});
// Test long-running process
test("long-running process", async (t) => {
await spawn("node", ["-e", `
setTimeout(() => {
console.log("completed");
process.exit(0);
}, 1000);
`], {
timeout: 2000,
expect: {
code: 0,
stdout: "completed\n"
}
});
});
// Test process with multiple outputs
test("process with stderr and stdout", async (t) => {
await spawn("node", ["-e", `
console.log("stdout message");
console.error("stderr message");
`], {
expect: {
code: 0,
stdout: "stdout message\n",
stderr: "stderr message\n"
}
});
});Test processes that require interaction or respond to signals.
// Test interactive process
test("interactive process", async (t) => {
const child = spawn("node", ["./interactive-cli.js"], {
stdio: "pipe",
timeout: 5000
});
// Send input over time
child.stdin.write("command1\n");
setTimeout(() => {
child.stdin.write("command2\n");
}, 100);
setTimeout(() => {
child.stdin.write("exit\n");
}, 200);
await child;
t.match(child.stdout, /response1/);
t.match(child.stdout, /response2/);
});
// Test signal handling
test("process signal handling", async (t) => {
await spawn("node", ["-e", `
process.on('SIGTERM', () => {
console.log('graceful shutdown');
process.exit(0);
});
setTimeout(() => {}, 10000); // Keep alive
`], {
signal: "SIGTERM",
timeout: 1000,
expect: {
stdout: "graceful shutdown\n"
}
});
});Comprehensive patterns for testing command-line applications.
// Test CLI with various arguments
test("CLI argument parsing", async (t) => {
// Test help flag
await spawn("./my-cli.js", ["--help"], {
expect: {
code: 0,
stdout: /Usage:/
}
});
// Test version flag
await spawn("./my-cli.js", ["--version"], {
expect: {
code: 0,
stdout: /\d+\.\d+\.\d+/
}
});
// Test invalid argument
await spawn("./my-cli.js", ["--invalid"], {
expect: {
code: 1,
stderr: /Unknown option/
}
});
});
// Test CLI with file processing
test("CLI file processing", async (t) => {
// Create test input file
const inputFile = "./test-input.txt";
const outputFile = "./test-output.txt";
await spawn("./file-processor.js", [inputFile, outputFile], {
expect: {
code: 0,
stdout: /Successfully processed/
}
});
// Verify output file was created
const fs = require("fs");
t.ok(fs.existsSync(outputFile), "output file created");
});
// Test CLI with configuration
test("CLI with config file", async (t) => {
await spawn("./my-cli.js", ["--config", "./test-config.json"], {
expect: {
code: 0,
stdout: /Configuration loaded/
}
});
});Handle various error conditions and edge cases in subprocess testing.
// Test command not found
test("command not found", async (t) => {
await spawn("nonexistent-command", {
expect: {
code: 127 // Command not found
}
});
});
// Test timeout handling
test("process timeout", async (t) => {
try {
await spawn("sleep", ["10"], {
timeout: 1000 // 1 second timeout
});
t.fail("Should have timed out");
} catch (error) {
t.match(error.message, /timeout/);
}
});
// Test memory/resource limits
test("worker resource limits", async (t) => {
await worker("./memory-intensive-worker.js", {
resourceLimits: {
maxOldGenerationSizeMb: 100
},
expect: {
code: 0
}
});
});Helper patterns for common subprocess testing scenarios.
// Utility for testing multiple commands
function testCliCommands(commands: Array<{cmd: string, args: string[], expect: any}>) {
return Promise.all(commands.map(({cmd, args, expect}) =>
spawn(cmd, args, { expect })
));
}
test("batch CLI testing", async (t) => {
await testCliCommands([
{ cmd: "echo", args: ["test1"], expect: { stdout: "test1\n" } },
{ cmd: "echo", args: ["test2"], expect: { stdout: "test2\n" } },
{ cmd: "echo", args: ["test3"], expect: { stdout: "test3\n" } }
]);
});Install with Tessl CLI
npx tessl i tessl/npm-tap