# 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)