Files
markitect-main/history/ADHOC_Testting_Architecture_Enhancement_GAMEPLAN.md
2025-10-03 03:39:43 +02:00

1288 lines
42 KiB
Markdown

# Testing Architecture Enhancement - Gameplan
## Overview
This gameplan establishes a comprehensive testing architecture that supports the domain logic separation and data access pattern improvements while ensuring high code quality, maintainability, and confidence in changes across the MarkiTect codebase.
## Current Testing Architecture Problems
### 1. **Test Organization and Structure Issues**
#### **Inconsistent Test File Organization**
- **Problem**: Tests scattered across multiple directories without clear structure
- **Current**: Mix of `tests/` and module-specific test files
- **Impact**: Difficult to locate and maintain tests
#### **Poor Test Naming Conventions**
- **Problem**: Inconsistent naming patterns (e.g., `test_issue_11_*`, `test_issue_creator.py`)
- **Current**: Tests named after issue numbers rather than functionality
- **Impact**: Tests don't clearly indicate what they're testing
#### **Mixed Test Types**
- **Problem**: Unit tests, integration tests, and end-to-end tests mixed together
- **Current**: No clear separation between test types
- **Impact**: Slow test execution, unclear test purpose
### 2. **Test Coverage and Quality Issues**
#### **Missing Test Coverage Areas**
```python
# Current gaps identified:
- Domain logic testing (business rules not tested in isolation)
- Repository pattern testing (no mock strategies)
- Error handling scenarios (happy path bias)
- Performance and load testing (no performance regression detection)
- Configuration management testing (config scenarios not covered)
```
#### **Poor Test Isolation**
- **Problem**: Tests depend on external systems and state
- **Current**: Tests make real API calls, modify actual files
- **Impact**: Flaky tests, slow execution, test interference
### 3. **Testing Anti-patterns Identified**
#### **Services Module Testing Issues**
```python
# Current anti-pattern in services/issue_service.py tests
class TestIssueService:
def test_get_issue_details(self):
# Problem: Real API calls in unit tests
service = IssueService()
result = service.get_issue_details(123) # Makes real HTTP request
assert result is not None
```
#### **TDDAI Module Testing Problems**
```python
# Current anti-pattern in tddai tests
class TestProjectManager:
def test_create_project(self):
# Problem: File system dependencies
manager = ProjectManager()
manager.create_workspace("/tmp/test") # Creates real directories
assert os.path.exists("/tmp/test") # Depends on file system state
```
## Testing Architecture Strategy
### **Test Pyramid Implementation**
```
E2E Tests (Few)
├─ Workflow Tests
├─ CLI Integration Tests
└─ API Integration Tests
Integration Tests (Some)
├─ Service Layer Tests
├─ Repository Tests
├─ Database Tests
└─ External API Tests
Unit Tests (Many)
├─ Domain Model Tests
├─ Business Logic Tests
├─ Value Object Tests
└─ Utility Function Tests
```
### **Testing Layer Architecture**
```python
tests/
unit/ # Fast, isolated unit tests
domain/ # Domain model and business logic tests
application/ # Application service tests (mocked repos)
infrastructure/ # Infrastructure component tests
integration/ # Integration tests with real components
repositories/ # Repository tests with real databases
services/ # Service tests with real dependencies
external/ # External API integration tests
e2e/ # End-to-end workflow tests
cli/ # CLI command testing
workflows/ # Complete user workflows
performance/ # Performance and load tests
fixtures/ # Test data and builders
markdown_samples.py
api_responses.py
database_seeds.py
utils/ # Test utilities and helpers
test_builders.py
mock_factories.py
assertions.py
```
## Implementation Gameplan
### **Phase 1: Foundation and Infrastructure (Week 1-2)**
#### **Task 1.1: Test Organization and Structure**
```python
# Create standardized test directory structure
tests/
conftest.py # Global test configuration
pytest.ini # Pytest configuration
requirements-test.txt # Test dependencies
[organized structure as above]
```
#### **Task 1.2: Test Configuration Setup**
```python
# tests/conftest.py
import pytest
import tempfile
import shutil
from pathlib import Path
from unittest.mock import Mock
from typing import Generator
@pytest.fixture(scope="session")
def test_workspace() -> Generator[Path, None, None]:
"""Create isolated test workspace."""
temp_dir = tempfile.mkdtemp(prefix="markitect_test_")
workspace_path = Path(temp_dir)
yield workspace_path
shutil.rmtree(temp_dir)
@pytest.fixture
def mock_database():
"""Provide mocked database for testing."""
mock_db = Mock()
mock_cursor = Mock()
mock_db.cursor.return_value = mock_cursor
mock_db.execute.return_value = mock_cursor
mock_cursor.fetchone.return_value = None
mock_cursor.fetchall.return_value = []
return mock_db
@pytest.fixture
def mock_http_client():
"""Provide mocked HTTP client for API tests."""
mock_client = Mock()
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": "success"}
mock_client.get.return_value = mock_response
mock_client.post.return_value = mock_response
return mock_client
```
#### **Task 1.3: Test Data Builders and Factories**
```python
# tests/fixtures/markdown_samples.py
class MarkdownDocumentBuilder:
"""Builder pattern for creating test markdown documents."""
def __init__(self):
self.content_parts = []
self.metadata = {}
def with_heading(self, text: str, level: int = 1):
heading_marker = "#" * level
self.content_parts.append(f"{heading_marker} {text}")
return self
def with_paragraph(self, text: str):
self.content_parts.append(text)
return self
def with_metadata(self, key: str, value: str):
self.metadata[key] = value
return self
def build(self) -> str:
content = "\n\n".join(self.content_parts)
if self.metadata:
metadata_lines = [f"{k}: {v}" for k, v in self.metadata.items()]
content = "---\n" + "\n".join(metadata_lines) + "\n---\n\n" + content
return content
# tests/fixtures/api_responses.py
class GiteaApiResponseBuilder:
"""Builder for creating mock Gitea API responses."""
def __init__(self):
self.issue_data = {
"number": 1,
"title": "Test Issue",
"state": "open",
"labels": [],
"milestone": None,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
def with_number(self, number: int):
self.issue_data["number"] = number
return self
def with_title(self, title: str):
self.issue_data["title"] = title
return self
def with_labels(self, *labels: str):
self.issue_data["labels"] = [{"name": label} for label in labels]
return self
def build(self) -> dict:
return self.issue_data.copy()
```
**Deliverables:**
- [ ] Standardized test directory structure
- [ ] Global test configuration and fixtures
- [ ] Test data builders and factories
- [ ] Test utilities and helpers
**Risk Level**: Low (foundation work, no breaking changes)
### **Phase 2: Unit Testing Framework (Week 2-3)**
#### **Task 2.1: Domain Model Unit Tests**
```python
# tests/unit/domain/test_issue_models.py
import pytest
from domain.issues.models import Issue, Label, IssueState
class TestIssue:
"""Test Issue domain model behavior."""
def test_issue_creation_with_valid_data(self):
# Arrange
issue = Issue(
number=123,
title="Test Issue",
state=IssueState.OPEN,
labels=[Label("bug"), Label("priority:high")]
)
# Act & Assert
assert issue.number == 123
assert issue.title == "Test Issue"
assert issue.state == IssueState.OPEN
assert len(issue.labels) == 2
def test_issue_state_transition_rules(self):
# Arrange
issue = Issue(number=1, title="Test", state=IssueState.OPEN)
# Act
issue.close()
# Assert
assert issue.state == IssueState.CLOSED
assert issue.closed_at is not None
def test_issue_label_categorization(self):
# Arrange
issue = Issue(
number=1,
title="Test",
labels=[
Label("bug"), # type label
Label("priority:high"), # priority label
Label("status:ready"), # state label
Label("custom") # other label
]
)
# Act
categories = issue.categorize_labels()
# Assert
assert "bug" in categories.type_labels
assert "priority:high" in categories.priority_labels
assert "status:ready" in categories.state_labels
assert "custom" in categories.other_labels
```
#### **Task 2.2: Business Logic Unit Tests**
```python
# tests/unit/domain/test_issue_services.py
import pytest
from domain.issues.services import IssueStatusService
from domain.issues.models import Issue, Label, IssueState
class TestIssueStatusService:
"""Test business logic for issue status determination."""
@pytest.fixture
def service(self):
return IssueStatusService()
def test_determine_kanban_column_for_new_issue(self, service):
# Arrange
issue = Issue(
number=1,
title="New Issue",
state=IssueState.OPEN,
labels=[Label("status:new")]
)
# Act
column = service.determine_kanban_column(issue)
# Assert
assert column == "Todo"
def test_determine_kanban_column_for_in_progress_issue(self, service):
# Arrange
issue = Issue(
number=1,
title="In Progress Issue",
state=IssueState.OPEN,
labels=[Label("status:in-progress")]
)
# Act
column = service.determine_kanban_column(issue)
# Assert
assert column == "In Progress"
@pytest.mark.parametrize("labels,expected_priority", [
([Label("priority:low")], "Low"),
([Label("priority:medium")], "Medium"),
([Label("priority:high")], "High"),
([Label("priority:critical")], "Critical"),
([], "Medium"), # Default priority
])
def test_extract_priority_info(self, service, labels, expected_priority):
# Arrange
issue = Issue(number=1, title="Test", labels=labels)
# Act
priority = service.extract_priority_info(issue)
# Assert
assert priority.level == expected_priority
```
#### **Task 2.3: Application Service Unit Tests (with Mocks)**
```python
# tests/unit/application/test_issue_application_service.py
import pytest
from unittest.mock import Mock, AsyncMock
from application.issue_application_service import IssueApplicationService
from domain.issues.models import Issue, IssueState
from infrastructure.unit_of_work import UnitOfWork
class TestIssueApplicationService:
"""Test application service coordination logic."""
@pytest.fixture
def mock_uow(self):
uow = Mock(spec=UnitOfWork)
uow.issues = AsyncMock()
uow.projects = AsyncMock()
uow.__aenter__ = AsyncMock(return_value=uow)
uow.__aexit__ = AsyncMock(return_value=None)
return uow
@pytest.fixture
def service(self, mock_uow):
return IssueApplicationService(mock_uow)
async def test_get_issue_details_success(self, service, mock_uow):
# Arrange
issue = Issue(number=123, title="Test Issue", state=IssueState.OPEN)
project_info = Mock()
project_info.kanban_columns = ["Todo", "In Progress", "Done"]
mock_uow.issues.get_issue.return_value = issue
mock_uow.projects.get_issue_project_info.return_value = project_info
# Act
result = await service.get_issue_details(123)
# Assert
assert result.issue == issue
assert result.project_info == project_info
mock_uow.issues.get_issue.assert_called_once_with(123)
mock_uow.projects.get_issue_project_info.assert_called_once_with(123)
async def test_get_issue_details_issue_not_found(self, service, mock_uow):
# Arrange
from domain.issues.exceptions import IssueNotFoundError
mock_uow.issues.get_issue.side_effect = IssueNotFoundError("Issue not found")
# Act & Assert
with pytest.raises(IssueNotFoundError):
await service.get_issue_details(999)
```
**Deliverables:**
- [ ] Unit tests for all domain models
- [ ] Unit tests for business logic services
- [ ] Unit tests for application services with mocks
- [ ] Parameterized tests for edge cases
**Risk Level**: Low (isolated unit tests, no external dependencies)
### **Phase 3: Integration Testing Framework (Week 3-4)**
#### **Task 3.1: Repository Integration Tests**
```python
# tests/integration/repositories/test_gitea_issue_repository.py
import pytest
import aiohttp
from infrastructure.repositories.gitea_issue_repository import GiteaIssueRepository
from infrastructure.connection_manager import ConnectionManager
from tests.fixtures.api_responses import GiteaApiResponseBuilder
class TestGiteaIssueRepository:
"""Integration tests for Gitea API repository."""
@pytest.fixture
async def repository(self, test_config):
connection_manager = ConnectionManager(test_config)
repo = GiteaIssueRepository(connection_manager)
yield repo
await connection_manager.close()
@pytest.fixture
def mock_server(self, aioresponses):
"""Mock HTTP responses for integration tests."""
return aioresponses
async def test_get_issue_success(self, repository, mock_server):
# Arrange
issue_data = (GiteaApiResponseBuilder()
.with_number(123)
.with_title("Test Issue")
.with_labels("bug", "priority:high")
.build())
mock_server.get(
"http://test-gitea.com/api/v1/repos/test/repo/issues/123",
payload=issue_data
)
# Act
issue = await repository.get_issue(123)
# Assert
assert issue.number == 123
assert issue.title == "Test Issue"
assert len(issue.labels) == 2
async def test_get_issue_not_found(self, repository, mock_server):
# Arrange
mock_server.get(
"http://test-gitea.com/api/v1/repos/test/repo/issues/999",
status=404
)
# Act & Assert
from domain.issues.exceptions import IssueNotFoundError
with pytest.raises(IssueNotFoundError):
await repository.get_issue(999)
async def test_get_issue_with_retry_on_network_error(self, repository, mock_server):
# Arrange - First two requests fail, third succeeds
issue_data = GiteaApiResponseBuilder().with_number(123).build()
mock_server.get(
"http://test-gitea.com/api/v1/repos/test/repo/issues/123",
exception=aiohttp.ClientError("Network error")
)
mock_server.get(
"http://test-gitea.com/api/v1/repos/test/repo/issues/123",
exception=aiohttp.ClientError("Network error")
)
mock_server.get(
"http://test-gitea.com/api/v1/repos/test/repo/issues/123",
payload=issue_data
)
# Act
issue = await repository.get_issue(123)
# Assert
assert issue.number == 123
# Verify retry mechanism worked (3 calls total)
assert len(mock_server.requests) == 3
```
#### **Task 3.2: Database Integration Tests**
```python
# tests/integration/repositories/test_sqlite_document_repository.py
import pytest
import sqlite3
from pathlib import Path
from infrastructure.repositories.sqlite_document_repository import SqliteDocumentRepository
from domain.documents.models import Document
class TestSqliteDocumentRepository:
"""Integration tests for SQLite document repository."""
@pytest.fixture
def test_db_path(self, test_workspace):
return test_workspace / "test.db"
@pytest.fixture
def repository(self, test_db_path):
repo = SqliteDocumentRepository(test_db_path)
repo.initialize_schema()
yield repo
repo.close()
async def test_store_and_retrieve_document(self, repository):
# Arrange
document = Document(
filename="test.md",
content="# Test Document\nContent here",
ast_data={"type": "document", "children": []}
)
# Act
document_id = await repository.store_document(document)
retrieved = await repository.get_document(document_id)
# Assert
assert retrieved.filename == "test.md"
assert retrieved.content == "# Test Document\nContent here"
assert retrieved.ast_data["type"] == "document"
async def test_store_duplicate_filename_raises_error(self, repository):
# Arrange
document1 = Document(filename="duplicate.md", content="Content 1")
document2 = Document(filename="duplicate.md", content="Content 2")
# Act
await repository.store_document(document1)
# Assert
from infrastructure.exceptions import DocumentStoreError
with pytest.raises(DocumentStoreError) as exc_info:
await repository.store_document(document2)
assert "already exists" in str(exc_info.value)
async def test_transaction_rollback_on_error(self, repository):
# Arrange
document = Document(filename="test.md", content="Valid content")
# Simulate a database error during storage
with pytest.raises(sqlite3.Error):
async with repository.unit_of_work():
await repository.store_document(document)
# Force an error that should rollback the transaction
await repository.execute_raw_sql("INVALID SQL")
# Assert - Document should not be stored due to rollback
documents = await repository.list_all_documents()
assert len(documents) == 0
```
#### **Task 3.3: Service Integration Tests**
```python
# tests/integration/services/test_document_service_integration.py
import pytest
from pathlib import Path
from application.document_service import DocumentService
from infrastructure.unit_of_work import UnitOfWork
from tests.fixtures.markdown_samples import MarkdownDocumentBuilder
class TestDocumentServiceIntegration:
"""Integration tests for document service with real repositories."""
@pytest.fixture
def service(self, test_workspace):
uow = UnitOfWork(database_path=test_workspace / "test.db")
uow.initialize()
yield DocumentService(uow)
uow.close()
async def test_ingest_markdown_file_complete_workflow(self, service, test_workspace):
# Arrange
markdown_content = (MarkdownDocumentBuilder()
.with_heading("Test Document")
.with_paragraph("This is a test paragraph.")
.with_heading("Section 2", level=2)
.build())
test_file = test_workspace / "test.md"
test_file.write_text(markdown_content)
# Act
result = await service.ingest_file(test_file)
# Assert
assert result.document_id is not None
assert result.parse_time > 0
assert result.cache_path.exists()
# Verify document was stored correctly
document = await service.get_document(result.document_id)
assert document.filename == "test.md"
assert "Test Document" in document.content
assert document.ast_data is not None
async def test_bulk_ingestion_with_transaction(self, service, test_workspace):
# Arrange
files = []
for i in range(5):
content = f"# Document {i}\nContent for document {i}"
file_path = test_workspace / f"doc_{i}.md"
file_path.write_text(content)
files.append(file_path)
# Act
results = await service.ingest_bulk(files)
# Assert
assert len(results) == 5
for result in results:
assert result.document_id is not None
assert result.parse_time > 0
# Verify all documents are stored
all_docs = await service.list_documents()
assert len(all_docs) == 5
```
**Deliverables:**
- [ ] Repository integration tests with real databases/APIs
- [ ] Service integration tests with transaction testing
- [ ] Error handling and retry mechanism tests
- [ ] Performance and load integration tests
**Risk Level**: Medium (involves real external dependencies)
### **Phase 4: End-to-End Testing Framework (Week 4-5)**
#### **Task 4.1: CLI Command Testing**
```python
# tests/e2e/cli/test_issue_commands.py
import pytest
import subprocess
from pathlib import Path
class TestIssueCommands:
"""End-to-end tests for issue management CLI commands."""
@pytest.fixture
def isolated_environment(self, test_workspace):
"""Set up isolated environment for CLI testing."""
env = {
"MARKITECT_WORKSPACE_DIR": str(test_workspace),
"MARKITECT_GITEA_URL": "http://test-gitea.com",
"MARKITECT_REPO_OWNER": "test",
"MARKITECT_REPO_NAME": "repo"
}
return env
def test_issue_show_command(self, isolated_environment):
# Act
result = subprocess.run(
["python", "tddai_cli.py", "show-issue", "123"],
env=isolated_environment,
capture_output=True,
text=True
)
# Assert
assert result.returncode == 0
assert "Issue #123 Details" in result.stdout
assert "Title:" in result.stdout
assert "Status:" in result.stdout
def test_issue_start_workflow(self, isolated_environment):
# Act - Start working on an issue
result = subprocess.run(
["python", "tddai_cli.py", "start-issue", "456"],
env=isolated_environment,
capture_output=True,
text=True
)
# Assert
assert result.returncode == 0
assert "Starting work on issue #456" in result.stdout
# Verify workspace was created
workspace_path = Path(isolated_environment["MARKITECT_WORKSPACE_DIR"]) / "issue_456"
assert workspace_path.exists()
assert (workspace_path / "requirements.md").exists()
assert (workspace_path / "test_plan.md").exists()
def test_complete_issue_workflow(self, isolated_environment):
# Act - Complete workflow: start -> add tests -> finish
commands = [
["python", "tddai_cli.py", "start-issue", "789"],
["python", "tddai_cli.py", "add-test", "test_scenario"],
["python", "tddai_cli.py", "finish-issue"]
]
for cmd in commands:
result = subprocess.run(
cmd,
env=isolated_environment,
capture_output=True,
text=True
)
assert result.returncode == 0
# Assert - Workspace should be cleaned up
workspace_path = Path(isolated_environment["MARKITECT_WORKSPACE_DIR"]) / "issue_789"
assert not workspace_path.exists()
```
#### **Task 4.2: Workflow Integration Tests**
```python
# tests/e2e/workflows/test_document_processing_workflow.py
import pytest
from pathlib import Path
import asyncio
from application.document_service import DocumentService
from application.workspace_service import WorkspaceService
from infrastructure.unit_of_work import UnitOfWork
class TestDocumentProcessingWorkflow:
"""End-to-end tests for complete document processing workflows."""
@pytest.fixture
async def services(self, test_workspace):
uow = UnitOfWork(database_path=test_workspace / "test.db")
await uow.initialize()
doc_service = DocumentService(uow)
workspace_service = WorkspaceService(uow)
yield doc_service, workspace_service
await uow.close()
async def test_full_document_lifecycle(self, services, test_workspace):
doc_service, workspace_service = services
# Arrange - Create test documents
docs_dir = test_workspace / "documents"
docs_dir.mkdir()
# Create various document types
(docs_dir / "readme.md").write_text("# Project README\nDescription here")
(docs_dir / "api.md").write_text("# API Documentation\n## Endpoints")
(docs_dir / "guide.md").write_text("# User Guide\n### Getting Started")
# Act - Process all documents
ingestion_results = []
for md_file in docs_dir.glob("*.md"):
result = await doc_service.ingest_file(md_file)
ingestion_results.append(result)
# Generate workspace summary
workspace_summary = await workspace_service.generate_summary()
# Act - Search functionality
search_results = await doc_service.search_content("API")
# Assert - All documents processed
assert len(ingestion_results) == 3
for result in ingestion_results:
assert result.document_id is not None
assert result.parse_time > 0
# Assert - Workspace summary generated
assert workspace_summary.total_documents == 3
assert workspace_summary.total_size > 0
# Assert - Search functionality works
assert len(search_results) >= 1
assert any("api.md" in result.filename for result in search_results)
async def test_large_document_processing_performance(self, services, test_workspace):
doc_service, _ = services
# Arrange - Create large document (1MB)
from tests.fixtures.markdown_samples import LargeMarkdownGenerator
generator = LargeMarkdownGenerator()
large_content = generator.generate_document(size='1mb')
large_file = test_workspace / "large_document.md"
large_file.write_text(large_content)
# Act - Measure processing time
import time
start_time = time.time()
result = await doc_service.ingest_file(large_file)
processing_time = time.time() - start_time
# Assert - Performance requirements
assert result.document_id is not None
assert processing_time < 10.0 # Should process 1MB in under 10 seconds
assert result.parse_time < 5.0 # AST parsing should be under 5 seconds
# Verify cache was created for performance
assert result.cache_path.exists()
cache_size = result.cache_path.stat().st_size
assert cache_size > 0
```
#### **Task 4.3: Performance and Load Testing**
```python
# tests/e2e/performance/test_system_performance.py
import pytest
import asyncio
import time
import statistics
from concurrent.futures import ThreadPoolExecutor
from application.document_service import DocumentService
from tests.fixtures.markdown_samples import MarkdownDocumentBuilder
class TestSystemPerformance:
"""Performance and load testing for the system."""
@pytest.fixture
async def service(self, test_workspace):
from infrastructure.unit_of_work import UnitOfWork
uow = UnitOfWork(database_path=test_workspace / "perf_test.db")
await uow.initialize()
yield DocumentService(uow)
await uow.close()
async def test_concurrent_document_ingestion(self, service, test_workspace):
"""Test system behavior under concurrent load."""
# Arrange - Create multiple test documents
docs_dir = test_workspace / "concurrent_docs"
docs_dir.mkdir()
doc_files = []
for i in range(20):
content = (MarkdownDocumentBuilder()
.with_heading(f"Document {i}")
.with_paragraph(f"Content for document {i}")
.build())
doc_file = docs_dir / f"doc_{i}.md"
doc_file.write_text(content)
doc_files.append(doc_file)
# Act - Process documents concurrently
start_time = time.time()
tasks = [service.ingest_file(doc_file) for doc_file in doc_files]
results = await asyncio.gather(*tasks)
total_time = time.time() - start_time
# Assert - Performance requirements
assert len(results) == 20
assert all(result.document_id is not None for result in results)
# Should process 20 small documents in under 30 seconds
assert total_time < 30.0
# Calculate processing statistics
parse_times = [result.parse_time for result in results]
avg_parse_time = statistics.mean(parse_times)
max_parse_time = max(parse_times)
assert avg_parse_time < 1.0 # Average parse time under 1 second
assert max_parse_time < 5.0 # Max parse time under 5 seconds
async def test_memory_usage_under_load(self, service, test_workspace):
"""Test memory usage patterns during heavy processing."""
import psutil
import os
# Measure initial memory
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss
# Arrange - Create multiple large documents
from tests.fixtures.markdown_samples import LargeMarkdownGenerator
generator = LargeMarkdownGenerator()
large_docs = []
for i in range(5):
content = generator.generate_document(size='1mb')
doc_file = test_workspace / f"large_{i}.md"
doc_file.write_text(content)
large_docs.append(doc_file)
# Act - Process large documents
for doc_file in large_docs:
await service.ingest_file(doc_file)
# Measure final memory
final_memory = process.memory_info().rss
memory_increase = final_memory - initial_memory
memory_increase_mb = memory_increase / (1024 * 1024)
# Assert - Memory usage should be reasonable
# Should not use more than 100MB additional memory for 5MB of documents
assert memory_increase_mb < 100
print(f"Memory increase: {memory_increase_mb:.2f} MB")
@pytest.mark.slow
async def test_system_stability_over_time(self, service, test_workspace):
"""Long-running stability test."""
# Run continuous processing for 5 minutes
start_time = time.time()
duration = 300 # 5 minutes
operation_count = 0
errors = []
while time.time() - start_time < duration:
try:
# Create and process a document
content = (MarkdownDocumentBuilder()
.with_heading(f"Stability Test {operation_count}")
.with_paragraph("Long-running test content")
.build())
doc_file = test_workspace / f"stability_{operation_count}.md"
doc_file.write_text(content)
await service.ingest_file(doc_file)
operation_count += 1
# Small delay between operations
await asyncio.sleep(0.1)
except Exception as e:
errors.append(str(e))
# Assert - System should remain stable
error_rate = len(errors) / operation_count if operation_count > 0 else 1
assert error_rate < 0.01 # Less than 1% error rate
assert operation_count > 100 # Should process at least 100 operations
print(f"Operations completed: {operation_count}")
print(f"Error rate: {error_rate:.2%}")
```
**Deliverables:**
- [ ] CLI command end-to-end tests
- [ ] Complete workflow integration tests
- [ ] Performance and load testing framework
- [ ] System stability and reliability tests
**Risk Level**: Low-Medium (end-to-end tests, performance requirements)
### **Phase 5: Test Migration and Optimization (Week 5-6)**
#### **Task 5.1: Migrate Existing Tests**
```python
# Migration strategy for existing tests
# Example: Migrating tests/test_issue_creator.py
# Before (current structure)
class TestIssueCreator:
def test_create_issue_success(self):
creator = IssueCreator(auth_token="test-token")
result = creator.create_issue("Test Issue", "Description")
assert result is not None
# After (new structure)
# tests/unit/application/test_issue_creator.py
class TestIssueCreator:
@pytest.fixture
def mock_repository(self):
return Mock(spec=IssueRepository)
@pytest.fixture
def creator(self, mock_repository):
return IssueCreator(mock_repository)
async def test_create_issue_success(self, creator, mock_repository):
# Arrange
mock_repository.create_issue.return_value = Issue(number=123, title="Test Issue")
# Act
result = await creator.create_issue("Test Issue", "Description")
# Assert
assert result.number == 123
mock_repository.create_issue.assert_called_once()
```
#### **Task 5.2: Test Performance Optimization**
```python
# tests/utils/performance_optimization.py
import pytest
import asyncio
from typing import List, Callable
class TestPerformanceOptimizer:
"""Utilities for optimizing test execution performance."""
@staticmethod
def parallelize_tests(test_functions: List[Callable]):
"""Run multiple test functions in parallel."""
async def run_parallel():
tasks = [asyncio.create_task(test_func()) for test_func in test_functions]
return await asyncio.gather(*tasks)
return asyncio.run(run_parallel())
@staticmethod
def cache_expensive_fixtures():
"""Cache expensive test fixtures across test sessions."""
# Implementation for fixture caching
pass
# pytest configuration for performance
# pytest.ini
[tool:pytest]
addopts =
--strict-markers
--strict-config
--verbose
--tb=short
--cov=src
--cov-report=term-missing
--cov-report=html
--cov-fail-under=90
--maxfail=1
--durations=10
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
e2e: marks tests as end-to-end tests
performance: marks tests as performance tests
```
#### **Task 5.3: CI/CD Integration**
```yaml
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run unit tests
run: pytest tests/unit/ -v --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run integration tests
run: pytest tests/integration/ -v
e2e-tests:
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run end-to-end tests
run: pytest tests/e2e/ -v -m "not slow"
- name: Run performance tests
run: pytest tests/e2e/performance/ -v
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
```
**Deliverables:**
- [ ] Migration of all existing tests to new architecture
- [ ] Test performance optimization and parallelization
- [ ] CI/CD pipeline integration
- [ ] Test coverage and quality gates
**Risk Level**: Medium (migration changes, CI/CD dependencies)
### **Phase 6: Advanced Testing Features (Week 6-7)**
#### **Task 6.1: Property-Based Testing**
```python
# tests/property/test_markdown_processing.py
import pytest
from hypothesis import given, strategies as st
from domain.documents.models import Document
from application.document_service import DocumentService
class TestMarkdownProcessingProperties:
"""Property-based tests for markdown processing."""
@given(st.text(alphabet=st.characters(blacklist_categories=('Cc', 'Cs'))))
async def test_any_valid_text_can_be_processed(self, text, document_service):
"""Any valid unicode text should be processable."""
# Arrange
document = Document(filename="test.md", content=text)
# Act - Should not raise exception
result = await document_service.process_document(document)
# Assert
assert result is not None
assert result.ast_data is not None
@given(st.text(min_size=1, max_size=1000))
async def test_processing_is_deterministic(self, content, document_service):
"""Same content should always produce same AST."""
# Arrange
document = Document(filename="test.md", content=content)
# Act
result1 = await document_service.process_document(document)
result2 = await document_service.process_document(document)
# Assert
assert result1.ast_data == result2.ast_data
@given(st.lists(st.text(min_size=1), min_size=1, max_size=10))
async def test_batch_processing_order_independence(self, contents, document_service):
"""Batch processing should be order-independent."""
# Arrange
documents1 = [Document(f"doc_{i}.md", content) for i, content in enumerate(contents)]
documents2 = list(reversed(documents1))
# Act
results1 = await document_service.process_batch(documents1)
results2 = await document_service.process_batch(documents2)
# Assert - Results should be equivalent regardless of order
results1_by_name = {r.filename: r.ast_data for r in results1}
results2_by_name = {r.filename: r.ast_data for r in results2}
assert results1_by_name == results2_by_name
```
#### **Task 6.2: Mutation Testing**
```python
# tests/mutation/test_coverage_quality.py
"""
Mutation testing to verify test quality.
Uses mutmut or similar tools to verify tests catch logic errors.
"""
# Configuration for mutation testing
# pyproject.toml
[tool.mutmut]
paths_to_mutate = "src/"
backup = false
runner = "python -m pytest tests/unit/"
tests_dir = "tests/"
# Mutation testing command
# mutmut run --paths-to-mutate src/domain/
```
#### **Task 6.3: Contract Testing**
```python
# tests/contract/test_api_contracts.py
import pytest
from pact import Consumer, Provider
from application.issue_service import IssueService
class TestGiteaApiContract:
"""Contract tests for Gitea API integration."""
@pytest.fixture
def pact(self):
pact = Consumer('markitect').has_pact_with(Provider('gitea'))
pact.start()
yield pact
pact.stop()
def test_get_issue_contract(self, pact):
# Define expected interaction
expected = {
'number': 123,
'title': 'Test Issue',
'state': 'open',
'labels': [{'name': 'bug'}]
}
(pact
.given('issue 123 exists')
.upon_receiving('a request for issue 123')
.with_request('GET', '/api/v1/repos/test/repo/issues/123')
.will_respond_with(200, body=expected))
# Test the interaction
with pact:
issue_service = IssueService(base_url=pact.uri)
issue = issue_service.get_issue(123)
assert issue.number == 123
```
**Deliverables:**
- [ ] Property-based testing framework
- [ ] Mutation testing setup
- [ ] Contract testing for external APIs
- [ ] Advanced test analysis and reporting
**Risk Level**: Low (advanced features, non-breaking additions)
## Success Criteria and Metrics
### **Implementation Success Indicators:**
#### **Coverage Metrics:**
- **Unit Test Coverage**: >90% for domain and application layers
- **Integration Test Coverage**: >80% for infrastructure layer
- **E2E Test Coverage**: >70% for critical user workflows
#### **Performance Metrics:**
- **Unit Tests**: All execute in <30 seconds total
- **Integration Tests**: All execute in <5 minutes total
- **E2E Tests**: Critical workflows tested in <15 minutes
#### **Quality Metrics:**
- **Test Reliability**: <1% flakiness rate
- **Test Maintainability**: Clear organization and documentation
- **CI/CD Integration**: Tests run automatically on all commits
- **Error Detection**: Mutation testing score >85%
### **Test Architecture Benefits:**
#### **Developer Experience:**
- **Fast Feedback**: Unit tests provide immediate feedback
- **Reliable Tests**: Consistent results across environments
- **Easy Debugging**: Clear test failure messages and context
- **Comprehensive Coverage**: All critical paths tested
#### **System Quality:**
- **Regression Prevention**: Automated detection of breaking changes
- **Performance Monitoring**: Continuous performance validation
- **Error Handling**: Comprehensive error scenario testing
- **Stability Assurance**: Long-running stability validation
This comprehensive testing architecture enhancement gameplan provides a robust foundation for ensuring code quality, catching regressions early, and maintaining confidence in the system as it evolves through domain logic separation and data access improvements.