Files
gh-vanman2024-cli-builder-p…/skills/cli-testing-patterns/templates/pytest-integration-test.py
2025-11-30 09:04:14 +08:00

379 lines
13 KiB
Python

"""
Pytest Integration Test Template
Complete workflow testing for CLI applications using Click.testing.CliRunner
Tests multi-command workflows, state persistence, and end-to-end scenarios
"""
import pytest
import os
import json
import yaml
from pathlib import Path
from click.testing import CliRunner
from mycli.cli import cli
@pytest.fixture
def integration_runner():
"""Create runner with isolated filesystem for integration tests"""
runner = CliRunner()
with runner.isolated_filesystem():
yield runner
class TestDeploymentWorkflow:
"""Test complete deployment workflow"""
def test_full_deployment_workflow(self, integration_runner):
"""Should complete init -> configure -> build -> deploy workflow"""
runner = integration_runner
# Step 1: Initialize project
result = runner.invoke(cli, ['init', 'my-project'])
assert result.exit_code == 0
assert 'Project initialized' in result.output
assert os.path.exists('my-project')
# Step 2: Configure API key
os.chdir('my-project')
result = runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
assert result.exit_code == 0
# Step 3: Build project
result = runner.invoke(cli, ['build', '--production'])
assert result.exit_code == 0
assert 'Build successful' in result.output
# Step 4: Deploy to production
result = runner.invoke(cli, ['deploy', 'production'])
assert result.exit_code == 0
assert 'Deployed successfully' in result.output
def test_deployment_without_config_fails(self, integration_runner):
"""Should fail deployment without required configuration"""
runner = integration_runner
# Initialize but don't configure
runner.invoke(cli, ['init', 'my-project'])
os.chdir('my-project')
# Try to deploy without API key
result = runner.invoke(cli, ['deploy', 'production'])
assert result.exit_code != 0
assert 'api_key' in result.output.lower()
def test_deployment_rollback(self, integration_runner):
"""Should rollback failed deployment"""
runner = integration_runner
# Setup and deploy
runner.invoke(cli, ['init', 'my-project'])
os.chdir('my-project')
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
runner.invoke(cli, ['deploy', 'staging'])
# Rollback
result = runner.invoke(cli, ['rollback'])
assert result.exit_code == 0
assert 'Rollback successful' in result.output
class TestMultiEnvironmentWorkflow:
"""Test multi-environment configuration and deployment"""
def test_manage_multiple_environments(self, integration_runner):
"""Should manage dev, staging, and production environments"""
runner = integration_runner
runner.invoke(cli, ['init', 'multi-env-project'])
os.chdir('multi-env-project')
# Configure development
runner.invoke(cli, ['config', 'set', 'api_key', 'dev_key', '--env', 'development'])
runner.invoke(cli, ['config', 'set', 'base_url', 'https://dev.api.example.com', '--env', 'development'])
# Configure staging
runner.invoke(cli, ['config', 'set', 'api_key', 'staging_key', '--env', 'staging'])
runner.invoke(cli, ['config', 'set', 'base_url', 'https://staging.api.example.com', '--env', 'staging'])
# Configure production
runner.invoke(cli, ['config', 'set', 'api_key', 'prod_key', '--env', 'production'])
runner.invoke(cli, ['config', 'set', 'base_url', 'https://api.example.com', '--env', 'production'])
# Deploy to each environment
dev_result = runner.invoke(cli, ['deploy', 'development'])
assert dev_result.exit_code == 0
assert 'dev.api.example.com' in dev_result.output
staging_result = runner.invoke(cli, ['deploy', 'staging'])
assert staging_result.exit_code == 0
assert 'staging.api.example.com' in staging_result.output
prod_result = runner.invoke(cli, ['deploy', 'production'])
assert prod_result.exit_code == 0
assert 'api.example.com' in prod_result.output
def test_environment_isolation(self, integration_runner):
"""Should keep environment configurations isolated"""
runner = integration_runner
runner.invoke(cli, ['init', 'isolated-project'])
os.chdir('isolated-project')
# Set different values for each environment
runner.invoke(cli, ['config', 'set', 'timeout', '10', '--env', 'development'])
runner.invoke(cli, ['config', 'set', 'timeout', '30', '--env', 'production'])
# Verify values are isolated
dev_result = runner.invoke(cli, ['config', 'get', 'timeout', '--env', 'development'])
assert '10' in dev_result.output
prod_result = runner.invoke(cli, ['config', 'get', 'timeout', '--env', 'production'])
assert '30' in prod_result.output
class TestStatePersistence:
"""Test state management and persistence"""
def test_state_persistence_across_commands(self, integration_runner):
"""Should maintain state across multiple commands"""
runner = integration_runner
# Initialize state
result = runner.invoke(cli, ['state', 'init'])
assert result.exit_code == 0
# Set multiple state values
runner.invoke(cli, ['state', 'set', 'counter', '0'])
runner.invoke(cli, ['state', 'set', 'user', 'testuser'])
# Increment counter multiple times
for i in range(5):
runner.invoke(cli, ['increment'])
# Verify final state
result = runner.invoke(cli, ['state', 'get', 'counter'])
assert result.exit_code == 0
assert '5' in result.output
result = runner.invoke(cli, ['state', 'get', 'user'])
assert 'testuser' in result.output
def test_state_recovery_from_corruption(self, integration_runner):
"""Should recover from corrupted state file"""
runner = integration_runner
# Create valid state
runner.invoke(cli, ['state', 'init'])
runner.invoke(cli, ['state', 'set', 'key', 'value'])
# Corrupt the state file
with open('.mycli-state', 'w') as f:
f.write('invalid json {[}')
# Should detect corruption and recover
result = runner.invoke(cli, ['state', 'get', 'key'])
assert result.exit_code != 0
assert 'corrupt' in result.output.lower()
# Should be able to reset
result = runner.invoke(cli, ['state', 'reset'])
assert result.exit_code == 0
class TestPluginWorkflow:
"""Test plugin installation and usage"""
def test_plugin_lifecycle(self, integration_runner):
"""Should install, use, and uninstall plugins"""
runner = integration_runner
runner.invoke(cli, ['init', 'plugin-project'])
os.chdir('plugin-project')
# Install plugin
result = runner.invoke(cli, ['plugin', 'install', 'test-plugin'])
assert result.exit_code == 0
assert 'installed' in result.output.lower()
# Verify plugin is listed
result = runner.invoke(cli, ['plugin', 'list'])
assert 'test-plugin' in result.output
# Use plugin command
result = runner.invoke(cli, ['test-plugin:command', '--arg', 'value'])
assert result.exit_code == 0
# Uninstall plugin
result = runner.invoke(cli, ['plugin', 'uninstall', 'test-plugin'])
assert result.exit_code == 0
# Verify plugin is removed
result = runner.invoke(cli, ['plugin', 'list'])
assert 'test-plugin' not in result.output
def test_plugin_conflict_detection(self, integration_runner):
"""Should detect and handle plugin conflicts"""
runner = integration_runner
runner.invoke(cli, ['init', 'conflict-project'])
os.chdir('conflict-project')
# Install first plugin
runner.invoke(cli, ['plugin', 'install', 'plugin-a'])
# Try to install conflicting plugin
result = runner.invoke(cli, ['plugin', 'install', 'plugin-b'])
if 'conflict' in result.output.lower():
assert result.exit_code != 0
class TestDataMigration:
"""Test data migration workflows"""
def test_version_migration(self, integration_runner):
"""Should migrate data between versions"""
runner = integration_runner
# Create old version data
old_data = {
'version': 1,
'format': 'legacy',
'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 'legacy' not in new_data.get('format', '')
def test_migration_backup(self, integration_runner):
"""Should create backup during migration"""
runner = integration_runner
# Create data
data = {'version': 1, 'data': 'important'}
with open('data.json', 'w') as f:
json.dump(data, f)
# Migrate with backup
result = runner.invoke(cli, ['migrate', '--to', '2.0', '--backup'])
assert result.exit_code == 0
# Verify backup exists
assert os.path.exists('data.json.backup')
class TestConcurrentOperations:
"""Test handling of concurrent operations"""
def test_file_locking(self, integration_runner):
"""Should prevent concurrent modifications"""
runner = integration_runner
runner.invoke(cli, ['init', 'lock-project'])
os.chdir('lock-project')
# Create lock file
with open('.mycli.lock', 'w') as f:
f.write('locked')
# Try to run command that needs lock
result = runner.invoke(cli, ['deploy', 'production'])
assert result.exit_code != 0
assert 'lock' in result.output.lower()
def test_lock_timeout(self, integration_runner):
"""Should timeout waiting for lock"""
runner = integration_runner
runner.invoke(cli, ['init', 'timeout-project'])
os.chdir('timeout-project')
# Create stale lock
with open('.mycli.lock', 'w') as f:
import time
f.write(str(time.time() - 3600)) # 1 hour old
# Should detect stale lock and continue
result = runner.invoke(cli, ['build'])
assert result.exit_code == 0
class TestErrorRecovery:
"""Test error recovery and retry logic"""
def test_retry_on_failure(self, integration_runner):
"""Should retry failed operations"""
runner = integration_runner
runner.invoke(cli, ['init', 'retry-project'])
os.chdir('retry-project')
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
# Simulate failure and retry
result = runner.invoke(cli, ['deploy', 'staging', '--retry', '3'])
# Should attempt retry logic
def test_partial_failure_recovery(self, integration_runner):
"""Should recover from partial failures"""
runner = integration_runner
runner.invoke(cli, ['init', 'recovery-project'])
os.chdir('recovery-project')
# Create partial state
runner.invoke(cli, ['build', '--step', '1'])
runner.invoke(cli, ['build', '--step', '2'])
# Complete from last successful step
result = runner.invoke(cli, ['build', '--continue'])
assert result.exit_code == 0
class TestCompleteWorkflow:
"""Test complete end-to-end workflows"""
def test_full_project_lifecycle(self, integration_runner):
"""Should complete entire project lifecycle"""
runner = integration_runner
# Create project
result = runner.invoke(cli, ['create', 'full-project'])
assert result.exit_code == 0
os.chdir('full-project')
# Configure
runner.invoke(cli, ['config', 'set', 'api_key', 'your_key_here'])
runner.invoke(cli, ['config', 'set', 'region', 'us-west-1'])
# Add dependencies
result = runner.invoke(cli, ['add', 'dependency', 'package-name'])
assert result.exit_code == 0
# Build
result = runner.invoke(cli, ['build', '--production'])
assert result.exit_code == 0
# Test
result = runner.invoke(cli, ['test'])
assert result.exit_code == 0
# Deploy
result = runner.invoke(cli, ['deploy', 'production'])
assert result.exit_code == 0
# Verify deployment
result = runner.invoke(cli, ['status'])
assert result.exit_code == 0
assert 'deployed' in result.output.lower()