278 lines
6.9 KiB
Markdown
278 lines
6.9 KiB
Markdown
# Jest Advanced CLI Testing Example
|
|
|
|
Advanced testing patterns for CLI applications including mocking, fixtures, and integration tests.
|
|
|
|
## Advanced Patterns
|
|
|
|
### 1. Async Command Testing
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
1. **Use Test Fixtures**: Create reusable test data and cleanup functions
|
|
2. **Mock External Dependencies**: Never make real API calls or database connections
|
|
3. **Test Edge Cases**: Test boundary conditions, empty inputs, special characters
|
|
4. **Async Handling**: Use proper async/await or promises for async operations
|
|
5. **Cleanup**: Always cleanup temp files, reset mocks, restore environment
|
|
6. **Isolation**: Tests should not depend on execution order
|
|
7. **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
|
|
|
|
## Resources
|
|
|
|
- [Jest Advanced Features](https://jestjs.io/docs/advanced)
|
|
- [Mocking with Jest](https://jestjs.io/docs/mock-functions)
|
|
- [Snapshot Testing](https://jestjs.io/docs/snapshot-testing)
|