1288 lines
42 KiB
Markdown
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. |