3.6 KiB
3.6 KiB
Jest Basic CLI Testing Example
This example demonstrates basic CLI testing patterns using Jest for Node.js/TypeScript projects.
Setup
npm install --save-dev jest @types/jest ts-jest @types/node
Test Structure
import { execSync } from 'child_process';
import path from 'path';
describe('CLI Tool Tests', () => {
const CLI_PATH = path.join(__dirname, '../bin/mycli');
function runCLI(args: string) {
try {
const stdout = execSync(`${CLI_PATH} ${args}`, {
encoding: 'utf8',
stdio: 'pipe',
});
return { stdout, stderr: '', code: 0 };
} catch (error: any) {
return {
stdout: error.stdout || '',
stderr: error.stderr || '',
code: error.status || 1,
};
}
}
test('should display version', () => {
const { stdout, code } = runCLI('--version');
expect(code).toBe(0);
expect(stdout).toContain('1.0.0');
});
test('should display help', () => {
const { stdout, code } = runCLI('--help');
expect(code).toBe(0);
expect(stdout).toContain('Usage:');
});
test('should handle unknown command', () => {
const { stderr, code } = runCLI('unknown-command');
expect(code).toBe(1);
expect(stderr).toContain('unknown command');
});
});
Running Tests
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run in watch mode
npm run test:watch
Key Patterns
1. Command Execution Helper
Create a reusable runCLI() function that:
- Executes CLI commands using
execSync - Captures stdout, stderr, and exit codes
- Handles both success and failure cases
2. Exit Code Testing
Always test exit codes:
0for success- Non-zero for errors
- Specific codes for different error types
3. Output Validation
Test output content using Jest matchers:
.toContain()for substring matching.toMatch()for regex patterns.toBe()for exact matches
4. Error Handling
Test error scenarios:
- Unknown commands
- Invalid options
- Missing required arguments
- Invalid argument types
Example Test Cases
describe('deploy command', () => {
test('should deploy with valid arguments', () => {
const { stdout, code } = runCLI('deploy production --force');
expect(code).toBe(0);
expect(stdout).toContain('Deploying to production');
});
test('should fail without required arguments', () => {
const { stderr, code } = runCLI('deploy');
expect(code).toBe(1);
expect(stderr).toContain('missing required argument');
});
test('should validate environment names', () => {
const { stderr, code } = runCLI('deploy invalid-env');
expect(code).toBe(1);
expect(stderr).toContain('invalid environment');
});
});
Best Practices
- Isolate Tests: Each test should be independent
- Use Descriptive Names: Test names should describe what they validate
- Test Both Success and Failure: Cover happy path and error cases
- Mock External Dependencies: Don't make real API calls or file system changes
- Use Type Safety: Leverage TypeScript for better test reliability
- Keep Tests Fast: Fast tests encourage frequent running
Common Pitfalls
- ❌ Not testing exit codes
- ❌ Only testing success cases
- ❌ Hardcoding paths instead of using
path.join() - ❌ Not handling async operations properly
- ❌ Testing implementation details instead of behavior