Implementation:DevExpress Testcafe TestedApp Start Kill
| Knowledge Sources | |
|---|---|
| Domains | Testing, CI_CD, Web_Automation |
| Last Updated | 2026-02-12 04:00 GMT |
Overview
Process management class for starting and terminating the application under test, with stdout/stderr logging and error detection.
Description
The TestedApp class provides lifecycle management for the application being tested. It uses the execa library to spawn application processes with shell command parsing, augments the PATH environment variable to include node_modules/.bin for local tool access, captures and logs stdout/stderr streams via debug loggers, monitors for premature process termination, implements configurable initialization delays, and uses tree-kill to terminate the entire process tree with SIGTERM.
Key implementation details:
- Process Spawning: Uses `execa.command()` in shell mode for command string parsing
- Environment Setup: Prepends node_modules/.bin to PATH for local binary resolution
- Stream Logging: Captures stdout/stderr with debug namespaces (testcafe:tested-app:stdout/stderr)
- Error Promise: Creates promise that rejects if application crashes unexpectedly
- Initialization Race: Waits for either initDelay timeout or application error
- Graceful Shutdown: Uses tree-kill with SIGTERM to stop entire process hierarchy
- Crash Suppression: Sets _killed flag to ignore expected termination errors
Usage
Use this implementation when:
- Running tests against local development servers (Express, webpack-dev-server, etc.)
- Configuring TestCafe to automatically start tested applications
- Debugging application startup issues in test contexts
- Ensuring clean application state between test runs
- Managing application lifecycle in CI pipelines
Code Reference
Source Location
- Repository: testcafe
- File: src/runner/tested-app.ts
- Lines: 39-98
Signature
class TestedApp {
private _process: null | ChildProcess;
private _killed: boolean;
private _stdoutLogger: debug.Debugger;
private _stderrLogger: debug.Debugger;
public errorPromise: null | Promise<void>;
constructor()
async start(command: string, initDelay: number): Promise<void>
async kill(): Promise<void>
private async _run(command: string): Promise<void>
}
Import
// Internal TestCafe usage
import TestedApp from './runner/tested-app';
// Usage in test runner
const app = new TestedApp();
await app.start('node server.js', 3000);
// ... run tests ...
await app.kill();
I/O Contract
Inputs
| Name | Type | Required | Description |
|---|---|---|---|
| command | string | Yes | Shell command to start application (e.g., "node server.js") |
| initDelay | number | Yes | Milliseconds to wait for app initialization (e.g., 3000) |
Outputs
| Name | Type | Description |
|---|---|---|
| stdout | logged | Application stdout captured to debug logger |
| stderr | logged | Application stderr captured to debug logger |
| errorPromise | Promise<void> | Promise that rejects if app crashes |
| exit_code | number | Process exit code (available after kill()) |
Usage Examples
CLI Usage
# Start Node.js server before tests
testcafe chrome tests/ \
--app "node server.js" \
--app-init-delay 3000
# Start with npm script
testcafe chrome tests/ \
--app "npm start" \
--app-init-delay 5000
# Start webpack-dev-server
testcafe chrome tests/ \
--app "webpack serve --port 8080" \
--app-init-delay 10000
# Start with environment variables
testcafe chrome tests/ \
--app "PORT=3000 NODE_ENV=test node server.js" \
--app-init-delay 2000
Configuration File
// .testcaferc.js
module.exports = {
src: 'tests/**/*.js',
browsers: 'chrome:headless',
// Application lifecycle
appCommand: 'node server.js',
appInitDelay: 3000, // 3 seconds
// Alternative: npm script
// appCommand: 'npm run dev',
// appInitDelay: 5000
};
Programmatic API
const createTestCafe = require('testcafe');
(async () => {
const testcafe = await createTestCafe('localhost', 1337, 1338);
const runner = testcafe.createRunner();
const failedCount = await runner
.src('tests/**/*.js')
.browsers('chrome:headless')
.startApp('node server.js', 3000) // Start app with 3s delay
.run();
console.log('Tests failed: ' + failedCount);
await testcafe.close(); // Automatically kills app
})();
Internal Implementation Flow
// How TestCafe uses TestedApp internally
// 1. Create TestedApp instance
const testedApp = new TestedApp();
// 2. Start application before tests
await testedApp.start('node server.js', 3000);
try {
// 3. Run tests while application is running
await runTests();
// 4. Check if app crashed during tests
if (testedApp.errorPromise) {
// errorPromise rejects if app crashed
await Promise.race([
runTests(),
testedApp.errorPromise
]);
}
} finally {
// 5. Always kill application after tests
await testedApp.kill();
}
Debugging Application Startup
# Enable debug logging to see app stdout/stderr
DEBUG=testcafe:tested-app:* testcafe chrome tests/ \
--app "node server.js" \
--app-init-delay 3000
# Output:
# testcafe:tested-app:stdout Server listening on port 3000
# testcafe:tested-app:stdout Database connected
# testcafe:tested-app:stdout Routes initialized
Error Scenarios
// Scenario 1: Application fails to start (syntax error)
// appCommand: "node server-with-error.js"
// Error: RUNTIME_ERRORS.testedAppFailedWithError
// Message: "SyntaxError: Unexpected token..."
// Tests never run
// Scenario 2: Application crashes during tests
try {
await app.start('node unstable-server.js', 1000);
await runTests();
} catch (error) {
// If app.errorPromise rejects during tests:
// Error: "Application crashed unexpectedly"
// Current test fails
}
// Scenario 3: Application port conflict
// appCommand: "node server.js" (port 3000 already in use)
// Error: "listen EADDRINUSE: address already in use :::3000"
// Tests never run
Process Tree Example
# Application spawns child processes
# appCommand: "npm start"
# Process tree:
# sh -c "npm start" (PID 1000)
# └─ npm (PID 1001)
# └─ node server.js (PID 1002)
# ├─ worker thread (PID 1003)
# └─ cluster worker (PID 1004)
# app.kill() uses tree-kill to terminate all:
# Kills: 1004, 1003, 1002, 1001, 1000 (depth-first)
# Signal: SIGTERM (graceful shutdown)
PATH Augmentation Behavior
// Before PATH augmentation:
// PATH=/usr/bin:/usr/local/bin
// Command: "webpack serve"
// Resolves to: /usr/local/bin/webpack (global install)
// After PATH augmentation by TestedApp:
// PATH=/project/node_modules/.bin:/usr/bin:/usr/local/bin
// Command: "webpack serve"
// Resolves to: /project/node_modules/.bin/webpack (local install)
// This ensures local project tools are used, not global versions
Initialization Delay Guidelines
// Choose initDelay based on application type:
// Static file server (serve, http-server)
appInitDelay: 100 // Very fast, just bind to port
// Express API (no database)
appInitDelay: 1000 // Fast, load routes
// Express API (with database)
appInitDelay: 3000 // Medium, connect to DB
// Full-stack app (Webpack Dev Server, Next.js)
appInitDelay: 10000 // Slow, compile assets
// Legacy app (complex initialization)
appInitDelay: 30000 // Very slow, multiple services
// Too short: Tests may fail because app not ready
// Too long: Wastes time waiting unnecessarily
// Best practice: Use smallest delay that reliably works
CI Pipeline Example
# GitHub Actions
- name: Run TestCafe tests
run: |
testcafe chrome:headless tests/ \
--app "node server.js" \
--app-init-delay 3000 \
--reporter xunit:junit.xml
# Application automatically:
# 1. Starts before tests
# 2. Runs during tests
# 3. Terminates after tests (even if tests fail)
# 4. Cleans up all child processes