Initial commit
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
# Integration Testing for CLI Applications
|
||||
|
||||
Complete workflows and integration testing patterns for CLI applications.
|
||||
|
||||
## Overview
|
||||
|
||||
Integration tests verify that multiple CLI commands work together correctly, testing complete user workflows rather than individual commands in isolation.
|
||||
|
||||
## Key Differences from Unit Tests
|
||||
|
||||
| Unit Tests | Integration Tests |
|
||||
|------------|-------------------|
|
||||
| Test individual commands | Test command sequences |
|
||||
| Mock external dependencies | May use real dependencies |
|
||||
| Fast execution | Slower execution |
|
||||
| Isolated state | Shared state across commands |
|
||||
|
||||
## Node.js Integration Testing
|
||||
|
||||
### Multi-Command Workflow
|
||||
|
||||
```typescript
|
||||
describe('Complete Deployment Workflow', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-integration-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('full deployment workflow', () => {
|
||||
// Step 1: Initialize project
|
||||
let result = runCLI(`init my-project --cwd ${tempDir}`);
|
||||
expect(result.code).toBe(0);
|
||||
expect(fs.existsSync(path.join(tempDir, 'my-project'))).toBe(true);
|
||||
|
||||
// Step 2: Configure
|
||||
const projectDir = path.join(tempDir, 'my-project');
|
||||
result = runCLI(`config set api_key test_key --cwd ${projectDir}`);
|
||||
expect(result.code).toBe(0);
|
||||
|
||||
// Step 3: Build
|
||||
result = runCLI(`build --production --cwd ${projectDir}`);
|
||||
expect(result.code).toBe(0);
|
||||
expect(fs.existsSync(path.join(projectDir, 'dist'))).toBe(true);
|
||||
|
||||
// Step 4: Deploy
|
||||
result = runCLI(`deploy staging --cwd ${projectDir}`);
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('Deployed successfully');
|
||||
|
||||
// Step 5: Verify
|
||||
result = runCLI(`status --cwd ${projectDir}`);
|
||||
expect(result.code).toBe(0);
|
||||
expect(result.stdout).toContain('staging');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### State Persistence Testing
|
||||
|
||||
```typescript
|
||||
describe('State Persistence', () => {
|
||||
test('state persists across commands', () => {
|
||||
const workspace = createTempWorkspace();
|
||||
|
||||
try {
|
||||
// Create initial state
|
||||
runCLI(`init --cwd ${workspace}`);
|
||||
runCLI(`config set key1 value1 --cwd ${workspace}`);
|
||||
runCLI(`config set key2 value2 --cwd ${workspace}`);
|
||||
|
||||
// Verify state persists
|
||||
let result = runCLI(`config get key1 --cwd ${workspace}`);
|
||||
expect(result.stdout).toContain('value1');
|
||||
|
||||
// Modify state
|
||||
runCLI(`config set key1 updated --cwd ${workspace}`);
|
||||
|
||||
// Verify modification
|
||||
result = runCLI(`config get key1 --cwd ${workspace}`);
|
||||
expect(result.stdout).toContain('updated');
|
||||
|
||||
// Verify other keys unchanged
|
||||
result = runCLI(`config get key2 --cwd ${workspace}`);
|
||||
expect(result.stdout).toContain('value2');
|
||||
} finally {
|
||||
cleanupWorkspace(workspace);
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Python Integration Testing
|
||||
|
||||
### Complete Workflow Testing
|
||||
|
||||
```python
|
||||
class TestCompleteWorkflow:
|
||||
"""Test complete CLI workflows"""
|
||||
|
||||
def test_project_lifecycle(self, runner):
|
||||
"""Test complete project lifecycle"""
|
||||
with runner.isolated_filesystem():
|
||||
# Initialize
|
||||
result = runner.invoke(cli, ['create', 'test-project'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Enter project directory
|
||||
os.chdir('test-project')
|
||||
|
||||
# Configure
|
||||
result = runner.invoke(cli, ['config', 'set', 'api_key', 'test_key'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Add dependencies
|
||||
result = runner.invoke(cli, ['add', 'dependency', 'requests'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Build
|
||||
result = runner.invoke(cli, ['build'])
|
||||
assert result.exit_code == 0
|
||||
assert os.path.exists('dist')
|
||||
|
||||
# Test
|
||||
result = runner.invoke(cli, ['test'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Deploy
|
||||
result = runner.invoke(cli, ['deploy', 'staging'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify
|
||||
result = runner.invoke(cli, ['status'])
|
||||
assert result.exit_code == 0
|
||||
assert 'staging' in result.output
|
||||
|
||||
def test_multi_environment_workflow(self, runner):
|
||||
"""Test workflow across multiple environments"""
|
||||
with runner.isolated_filesystem():
|
||||
# Setup
|
||||
runner.invoke(cli, ['init', 'multi-env-app'])
|
||||
os.chdir('multi-env-app')
|
||||
|
||||
# Configure environments
|
||||
environments = ['development', 'staging', 'production']
|
||||
|
||||
for env in environments:
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
['config', 'set', 'api_key', f'{env}_key', '--env', env]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Deploy to each environment
|
||||
for env in environments:
|
||||
result = runner.invoke(cli, ['deploy', env])
|
||||
assert result.exit_code == 0
|
||||
assert env in result.output
|
||||
```
|
||||
|
||||
### Error Recovery Testing
|
||||
|
||||
```python
|
||||
class TestErrorRecovery:
|
||||
"""Test error recovery workflows"""
|
||||
|
||||
def test_rollback_on_failure(self, runner):
|
||||
"""Test rollback after failed deployment"""
|
||||
with runner.isolated_filesystem():
|
||||
# Setup
|
||||
runner.invoke(cli, ['init', 'rollback-test'])
|
||||
os.chdir('rollback-test')
|
||||
runner.invoke(cli, ['config', 'set', 'api_key', 'test_key'])
|
||||
|
||||
# Successful deployment
|
||||
result = runner.invoke(cli, ['deploy', 'staging'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Failed deployment (simulate)
|
||||
result = runner.invoke(cli, ['deploy', 'staging', '--force-fail'])
|
||||
assert result.exit_code != 0
|
||||
|
||||
# Rollback
|
||||
result = runner.invoke(cli, ['rollback'])
|
||||
assert result.exit_code == 0
|
||||
assert 'rollback successful' in result.output.lower()
|
||||
|
||||
def test_recovery_from_corruption(self, runner):
|
||||
"""Test recovery from corrupted state"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create valid state
|
||||
runner.invoke(cli, ['init', 'corrupt-test'])
|
||||
os.chdir('corrupt-test')
|
||||
runner.invoke(cli, ['config', 'set', 'key', 'value'])
|
||||
|
||||
# Corrupt state file
|
||||
with open('.cli-state', 'w') as f:
|
||||
f.write('invalid json {[}')
|
||||
|
||||
# Should detect and recover
|
||||
result = runner.invoke(cli, ['config', 'get', 'key'])
|
||||
assert result.exit_code != 0
|
||||
assert 'corrupt' in result.output.lower()
|
||||
|
||||
# Reset state
|
||||
result = runner.invoke(cli, ['reset', '--force'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Should work after reset
|
||||
result = runner.invoke(cli, ['config', 'set', 'key', 'new_value'])
|
||||
assert result.exit_code == 0
|
||||
```
|
||||
|
||||
## Integration Test Patterns
|
||||
|
||||
### 1. Sequential Command Testing
|
||||
|
||||
Test commands that must run in a specific order:
|
||||
|
||||
```python
|
||||
def test_sequential_workflow(runner):
|
||||
"""Test commands that depend on each other"""
|
||||
with runner.isolated_filesystem():
|
||||
# Each command depends on the previous
|
||||
commands = [
|
||||
['init', 'project'],
|
||||
['config', 'set', 'key', 'value'],
|
||||
['build'],
|
||||
['test'],
|
||||
['deploy', 'staging']
|
||||
]
|
||||
|
||||
for cmd in commands:
|
||||
result = runner.invoke(cli, cmd)
|
||||
assert result.exit_code == 0, \
|
||||
f"Command {' '.join(cmd)} failed: {result.output}"
|
||||
```
|
||||
|
||||
### 2. Concurrent Operation Testing
|
||||
|
||||
Test that concurrent operations are handled correctly:
|
||||
|
||||
```python
|
||||
def test_concurrent_operations(runner):
|
||||
"""Test handling of concurrent operations"""
|
||||
import threading
|
||||
|
||||
results = []
|
||||
|
||||
def run_command():
|
||||
result = runner.invoke(cli, ['deploy', 'staging'])
|
||||
results.append(result)
|
||||
|
||||
# Start multiple deployments
|
||||
threads = [threading.Thread(target=run_command) for _ in range(3)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
# Only one should succeed, others should detect lock
|
||||
successful = sum(1 for r in results if r.exit_code == 0)
|
||||
assert successful == 1
|
||||
assert any('locked' in r.output.lower() for r in results if r.exit_code != 0)
|
||||
```
|
||||
|
||||
### 3. Data Migration Testing
|
||||
|
||||
Test data migration between versions:
|
||||
|
||||
```python
|
||||
def test_data_migration(runner):
|
||||
"""Test data migration workflow"""
|
||||
with runner.isolated_filesystem():
|
||||
# Create old version data
|
||||
old_data = {'version': 1, 'data': {'key': 'value'}}
|
||||
with open('data.json', 'w') as f:
|
||||
json.dump(old_data, f)
|
||||
|
||||
# Run migration
|
||||
result = runner.invoke(cli, ['migrate', '--to', '2.0'])
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify new format
|
||||
with open('data.json', 'r') as f:
|
||||
new_data = json.load(f)
|
||||
assert new_data['version'] == 2
|
||||
assert new_data['data']['key'] == 'value'
|
||||
|
||||
# Verify backup created
|
||||
assert os.path.exists('data.json.backup')
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Isolated Environments**: Each test should run in a clean environment
|
||||
2. **Test Real Workflows**: Test actual user scenarios, not artificial sequences
|
||||
3. **Include Error Paths**: Test recovery from failures
|
||||
4. **Test State Persistence**: Verify data persists correctly across commands
|
||||
5. **Use Realistic Data**: Test with data similar to production use cases
|
||||
6. **Clean Up Resources**: Always cleanup temp files and resources
|
||||
7. **Document Workflows**: Clearly document what workflow each test verifies
|
||||
8. **Set Appropriate Timeouts**: Integration tests may take longer
|
||||
9. **Mark Slow Tests**: Use test markers for slow-running integration tests
|
||||
10. **Test Concurrency**: Verify handling of simultaneous operations
|
||||
|
||||
## Running Integration Tests
|
||||
|
||||
### Node.js/Jest
|
||||
|
||||
```bash
|
||||
# Run all integration tests
|
||||
npm test -- --testPathPattern=integration
|
||||
|
||||
# Run specific integration test
|
||||
npm test -- integration/deployment.test.ts
|
||||
|
||||
# Run with extended timeout
|
||||
npm test -- --testTimeout=30000
|
||||
```
|
||||
|
||||
### Python/pytest
|
||||
|
||||
```bash
|
||||
# Run all integration tests
|
||||
pytest tests/integration
|
||||
|
||||
# Run specific test
|
||||
pytest tests/integration/test_workflow.py
|
||||
|
||||
# Run marked integration tests
|
||||
pytest -m integration
|
||||
|
||||
# Run with verbose output
|
||||
pytest tests/integration -v
|
||||
|
||||
# Skip slow tests
|
||||
pytest -m "not slow"
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Integration Testing Best Practices](https://martinfowler.com/bliki/IntegrationTest.html)
|
||||
- [Testing Strategies](https://testing.googleblog.com/)
|
||||
- [CLI Testing Patterns](https://clig.dev/#testing)
|
||||
Reference in New Issue
Block a user