6.9 KiB
6.9 KiB
Jest Advanced CLI Testing Example
Advanced testing patterns for CLI applications including mocking, fixtures, and integration tests.
Advanced Patterns
1. Async Command Testing
import { spawn } from 'child_process';
async function runCLIAsync(args: string[]): Promise<CLIResult> {
return new Promise((resolve) => {
const child = spawn(CLI_PATH, args, { stdio: 'pipe' });
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({ stdout, stderr, code: code || 0 });
});
});
}
test('should handle long-running command', async () => {
const result = await runCLIAsync(['deploy', 'production']);
expect(result.code).toBe(0);
}, 30000); // 30 second timeout
2. Environment Variable Mocking
describe('environment configuration', () => {
const originalEnv = { ...process.env };
afterEach(() => {
process.env = { ...originalEnv };
});
test('should use API key from environment', () => {
process.env.API_KEY = 'test_key_123';
const { stdout, code } = runCLI('status');
expect(code).toBe(0);
expect(stdout).toContain('Authenticated');
});
test('should fail without API key', () => {
delete process.env.API_KEY;
const { stderr, code } = runCLI('status');
expect(code).toBe(1);
expect(stderr).toContain('API key not found');
});
});
3. File System Fixtures
import fs from 'fs';
import os from 'os';
describe('config file handling', () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-test-'));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
test('should create config file', () => {
const configFile = path.join(tempDir, '.config');
const result = runCLI(`init --config ${configFile}`);
expect(result.code).toBe(0);
expect(fs.existsSync(configFile)).toBe(true);
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
expect(config).toHaveProperty('api_key');
});
});
4. Mocking External APIs
import nock from 'nock';
describe('API interaction', () => {
beforeEach(() => {
nock.cleanAll();
});
test('should fetch deployment status', () => {
nock('https://api.example.com')
.get('/deployments/123')
.reply(200, { status: 'success', environment: 'production' });
const { stdout, code } = runCLI('status --deployment 123');
expect(code).toBe(0);
expect(stdout).toContain('success');
expect(stdout).toContain('production');
});
test('should handle API errors', () => {
nock('https://api.example.com')
.get('/deployments/123')
.reply(500, { error: 'Internal Server Error' });
const { stderr, code } = runCLI('status --deployment 123');
expect(code).toBe(1);
expect(stderr).toContain('API error');
});
});
5. Test Fixtures
// test-fixtures.ts
export const createTestFixtures = () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-test-'));
// Create sample project structure
fs.mkdirSync(path.join(tempDir, 'src'));
fs.writeFileSync(
path.join(tempDir, 'package.json'),
JSON.stringify({ name: 'test-project', version: '1.0.0' })
);
return {
tempDir,
cleanup: () => fs.rmSync(tempDir, { recursive: true, force: true }),
};
};
// Usage in tests
test('should build project', () => {
const fixtures = createTestFixtures();
try {
const result = runCLI(`build --cwd ${fixtures.tempDir}`);
expect(result.code).toBe(0);
expect(fs.existsSync(path.join(fixtures.tempDir, 'dist'))).toBe(true);
} finally {
fixtures.cleanup();
}
});
6. Snapshot Testing
test('help output matches snapshot', () => {
const { stdout } = runCLI('--help');
expect(stdout).toMatchSnapshot();
});
test('version format matches snapshot', () => {
const { stdout } = runCLI('--version');
expect(stdout).toMatchSnapshot();
});
7. Parameterized Tests
describe.each([
['development', 'dev.example.com'],
['staging', 'staging.example.com'],
['production', 'api.example.com'],
])('deploy to %s', (environment, expectedUrl) => {
test(`should deploy to ${environment}`, () => {
const { stdout, code } = runCLI(`deploy ${environment}`);
expect(code).toBe(0);
expect(stdout).toContain(expectedUrl);
});
});
8. Interactive Command Testing
import { Readable, Writable } from 'stream';
test('should handle interactive prompts', (done) => {
const child = spawn(CLI_PATH, ['init'], { stdio: 'pipe' });
const inputs = ['my-project', 'John Doe', 'john@example.com'];
let inputIndex = 0;
child.stdout?.on('data', (data) => {
const output = data.toString();
if (output.includes('?') && inputIndex < inputs.length) {
child.stdin?.write(inputs[inputIndex] + '\n');
inputIndex++;
}
});
child.on('close', (code) => {
expect(code).toBe(0);
done();
});
});
9. Coverage-Driven Testing
// Ensure all CLI commands are tested
describe('CLI command coverage', () => {
const commands = ['init', 'build', 'deploy', 'status', 'config'];
commands.forEach((command) => {
test(`${command} command exists`, () => {
const { stdout } = runCLI('--help');
expect(stdout).toContain(command);
});
test(`${command} has help text`, () => {
const { stdout, code } = runCLI(`${command} --help`);
expect(code).toBe(0);
expect(stdout).toContain('Usage:');
});
});
});
10. Performance Testing
test('command executes within time limit', () => {
const startTime = Date.now();
const { code } = runCLI('status');
const duration = Date.now() - startTime;
expect(code).toBe(0);
expect(duration).toBeLessThan(2000); // Should complete within 2 seconds
});
Best Practices
- Use Test Fixtures: Create reusable test data and cleanup functions
- Mock External Dependencies: Never make real API calls or database connections
- Test Edge Cases: Test boundary conditions, empty inputs, special characters
- Async Handling: Use proper async/await or promises for async operations
- Cleanup: Always cleanup temp files, reset mocks, restore environment
- Isolation: Tests should not depend on execution order
- Clear Error Messages: Write assertions with helpful failure messages
Common Advanced Patterns
- Concurrent execution testing
- File locking and race conditions
- Signal handling (SIGTERM, SIGINT)
- Large file processing
- Streaming output
- Progress indicators
- Error recovery and retry logic