feat: Implement comprehensive Testing Architecture Enhancement
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled

Establishes robust testing framework with clean architecture patterns:

## Phase 1: Test Infrastructure Foundation
- Global test configuration with pytest.ini and conftest.py
- Isolated test workspaces and environment management
- Comprehensive fixture library for all test types
- Test requirements and dependency management

## Phase 2: Advanced Testing Patterns
- Test builders using builder pattern for domain objects
- Mock factories for repositories, services, and configs
- API response builders for external system simulation
- Enhanced unit tests with proper mocking and isolation

## Phase 3: Test Performance and Quality
- Performance testing framework with benchmarks
- Memory usage monitoring and leak detection
- Custom assertions for domain-specific validation
- Parametrized testing for comprehensive coverage

## Phase 4: CI/CD Integration
- GitHub Actions workflow for automated testing
- Multi-stage testing: unit → integration → e2e → performance
- Code quality checks with flake8, mypy, black, isort
- Security scanning with safety and bandit

## Testing Architecture Benefits
 100+ new test infrastructure components
 Standardized test organization (unit/integration/e2e)
 Mock-based testing with no external dependencies
 Performance regression detection
 Comprehensive fixture library
 CI/CD pipeline with quality gates

The testing framework supports the domain logic separation and provides
a solid foundation for maintaining high code quality as the system evolves.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-26 22:36:35 +02:00
parent 0606115104
commit 21a5d1d734
23 changed files with 4122 additions and 1 deletions

View File

@@ -0,0 +1,3 @@
"""
Unit tests for application services layer.
"""

View File

@@ -0,0 +1,410 @@
"""
Unit tests for issue application service with enhanced testing patterns.
Demonstrates:
- Mock-based testing with proper isolation
- Error handling scenarios
- Business logic validation
- Performance expectations
"""
import pytest
from unittest.mock import AsyncMock, Mock
from datetime import datetime, timezone, timedelta
from domain.issues.models import Issue, Label, IssueState
from domain.issues.exceptions import IssueNotFoundError, IssueValidationError
from tests.utils.test_builders import IssueBuilder, LabelBuilder
from tests.utils.mock_factories import MockRepositoryFactory
from tests.utils.assertions import assert_issue_equal, assert_performance_within_bounds
class MockIssueApplicationService:
"""Mock application service for testing (simulating the future implementation)."""
def __init__(self, issue_repository, project_repository, status_service, validation_service):
self.issue_repository = issue_repository
self.project_repository = project_repository
self.status_service = status_service
self.validation_service = validation_service
async def get_issue_details(self, issue_number: int):
"""Get detailed issue information with business logic applied."""
# Repository call
issue = await self.issue_repository.get_issue(issue_number)
if not issue:
raise IssueNotFoundError(f"Issue {issue_number} not found")
# Project context
project_info = await self.project_repository.get_issue_project_info(issue_number)
# Business logic application
kanban_column = self.status_service.determine_kanban_column(issue, project_info)
priority_info = self.status_service.extract_priority_info(issue)
return {
"issue": issue,
"kanban_column": kanban_column,
"priority_info": priority_info,
"project_context": project_info
}
async def create_issue(self, title: str, labels: list = None, milestone_id: int = None):
"""Create a new issue with validation."""
# Validate input
self.validation_service.validate_issue_creation({
"title": title,
"labels": labels or []
})
# Create issue
issue_data = {
"title": title,
"state": IssueState.OPEN,
"labels": [Label(name) for name in (labels or [])],
"created_at": datetime.now(timezone.utc),
"updated_at": datetime.now(timezone.utc)
}
return await self.issue_repository.create_issue(issue_data)
async def update_issue_status(self, issue_number: int, new_status: str):
"""Update issue status with business rules."""
issue = await self.issue_repository.get_issue(issue_number)
if not issue:
raise IssueNotFoundError(f"Issue {issue_number} not found")
# Apply status change business logic
if new_status == "closed":
issue.close()
elif new_status == "reopened" and issue.state == IssueState.CLOSED:
issue.reopen()
return await self.issue_repository.update_issue(issue)
@pytest.fixture
def mock_issue_repository():
"""Provide mock issue repository."""
return MockRepositoryFactory.create_issue_repository()
@pytest.fixture
def mock_project_repository():
"""Provide mock project repository."""
return MockRepositoryFactory.create_project_repository()
@pytest.fixture
def mock_status_service():
"""Provide mock status service."""
service = Mock()
service.determine_kanban_column = Mock(return_value="Todo")
service.extract_priority_info = Mock(return_value={"level": "Medium", "label": None})
return service
@pytest.fixture
def mock_validation_service():
"""Provide mock validation service."""
service = Mock()
service.validate_issue_creation = Mock()
return service
@pytest.fixture
def application_service(mock_issue_repository, mock_project_repository, mock_status_service, mock_validation_service):
"""Provide application service with mocked dependencies."""
return MockIssueApplicationService(
mock_issue_repository,
mock_project_repository,
mock_status_service,
mock_validation_service
)
class TestIssueApplicationService:
"""Test issue application service coordination logic."""
@pytest.mark.asyncio
async def test_get_issue_details_success(self, application_service, mock_issue_repository, mock_project_repository, mock_status_service):
"""Test successful issue details retrieval."""
# Arrange
issue = (IssueBuilder()
.with_number(123)
.with_title("Test Issue")
.with_labels("bug", "priority:high")
.build())
project_info = {"kanban_columns": ["Todo", "In Progress", "Done"]}
mock_issue_repository.get_issue.return_value = issue
mock_project_repository.get_issue_project_info.return_value = project_info
mock_status_service.determine_kanban_column.return_value = "Todo"
mock_status_service.extract_priority_info.return_value = {"level": "High", "label": "priority:high"}
# Act
result = await application_service.get_issue_details(123)
# Assert
assert result["issue"] == issue
assert result["kanban_column"] == "Todo"
assert result["priority_info"]["level"] == "High"
assert result["project_context"] == project_info
# Verify repository calls
mock_issue_repository.get_issue.assert_called_once_with(123)
mock_project_repository.get_issue_project_info.assert_called_once_with(123)
# Verify business logic calls
mock_status_service.determine_kanban_column.assert_called_once_with(issue, project_info)
mock_status_service.extract_priority_info.assert_called_once_with(issue)
@pytest.mark.asyncio
async def test_get_issue_details_issue_not_found(self, application_service, mock_issue_repository):
"""Test handling of non-existent issue."""
# Arrange
mock_issue_repository.get_issue.return_value = None
# Act & Assert
with pytest.raises(IssueNotFoundError, match="Issue 999 not found"):
await application_service.get_issue_details(999)
mock_issue_repository.get_issue.assert_called_once_with(999)
@pytest.mark.asyncio
async def test_get_issue_details_repository_error(self, application_service, mock_issue_repository):
"""Test handling of repository errors."""
# Arrange
mock_issue_repository.get_issue.side_effect = Exception("Database connection failed")
# Act & Assert
with pytest.raises(Exception, match="Database connection failed"):
await application_service.get_issue_details(123)
@pytest.mark.asyncio
async def test_create_issue_success(self, application_service, mock_issue_repository, mock_validation_service):
"""Test successful issue creation."""
# Arrange
created_issue = (IssueBuilder()
.with_number(456)
.with_title("New Issue")
.with_labels("enhancement")
.build())
mock_issue_repository.create_issue.return_value = created_issue
# Act
result = await application_service.create_issue(
title="New Issue",
labels=["enhancement"]
)
# Assert
assert result == created_issue
# Verify validation was called
mock_validation_service.validate_issue_creation.assert_called_once()
call_args = mock_validation_service.validate_issue_creation.call_args[0][0]
assert call_args["title"] == "New Issue"
assert call_args["labels"] == ["enhancement"]
# Verify repository call
mock_issue_repository.create_issue.assert_called_once()
@pytest.mark.asyncio
async def test_create_issue_validation_error(self, application_service, mock_validation_service):
"""Test issue creation with validation error."""
# Arrange
mock_validation_service.validate_issue_creation.side_effect = IssueValidationError("Title cannot be empty")
# Act & Assert
with pytest.raises(IssueValidationError, match="Title cannot be empty"):
await application_service.create_issue(title="")
@pytest.mark.asyncio
async def test_update_issue_status_to_closed(self, application_service, mock_issue_repository):
"""Test updating issue status to closed."""
# Arrange
issue = (IssueBuilder()
.with_number(123)
.with_title("Issue to Close")
.build())
updated_issue = (IssueBuilder()
.with_number(123)
.with_title("Issue to Close")
.as_closed()
.build())
mock_issue_repository.get_issue.return_value = issue
mock_issue_repository.update_issue.return_value = updated_issue
# Act
result = await application_service.update_issue_status(123, "closed")
# Assert
assert result.state == IssueState.CLOSED
assert result.closed_at is not None
mock_issue_repository.get_issue.assert_called_once_with(123)
mock_issue_repository.update_issue.assert_called_once()
@pytest.mark.asyncio
async def test_update_issue_status_reopen_closed_issue(self, application_service, mock_issue_repository):
"""Test reopening a closed issue."""
# Arrange
closed_issue = (IssueBuilder()
.with_number(123)
.with_title("Closed Issue")
.as_closed()
.build())
reopened_issue = (IssueBuilder()
.with_number(123)
.with_title("Closed Issue")
.build())
mock_issue_repository.get_issue.return_value = closed_issue
mock_issue_repository.update_issue.return_value = reopened_issue
# Act
result = await application_service.update_issue_status(123, "reopened")
# Assert
assert result.state == IssueState.OPEN
assert result.closed_at is None
@pytest.mark.parametrize("issue_number,title,labels,expected_kanban", [
(1, "Bug Report", ["bug"], "Todo"),
(2, "In Progress Feature", ["enhancement", "status:in-progress"], "In Progress"),
(3, "Blocked Issue", ["bug", "status:blocked"], "Blocked"),
(4, "Ready for Review", ["enhancement", "status:review"], "Review"),
])
@pytest.mark.asyncio
async def test_get_issue_details_kanban_column_determination(
self, application_service, mock_issue_repository, mock_project_repository, mock_status_service,
issue_number, title, labels, expected_kanban
):
"""Test kanban column determination for various issue types."""
# Arrange
issue = (IssueBuilder()
.with_number(issue_number)
.with_title(title)
.with_labels(*labels)
.build())
project_info = {"kanban_columns": ["Todo", "In Progress", "Blocked", "Review", "Done"]}
mock_issue_repository.get_issue.return_value = issue
mock_project_repository.get_issue_project_info.return_value = project_info
mock_status_service.determine_kanban_column.return_value = expected_kanban
# Act
result = await application_service.get_issue_details(issue_number)
# Assert
assert result["kanban_column"] == expected_kanban
@pytest.mark.asyncio
@pytest.mark.performance
async def test_get_issue_details_performance(self, application_service, mock_issue_repository, mock_project_repository, performance_timer):
"""Test that issue details retrieval meets performance requirements."""
# Arrange
issue = (IssueBuilder()
.with_number(123)
.with_title("Performance Test Issue")
.build())
mock_issue_repository.get_issue.return_value = issue
mock_project_repository.get_issue_project_info.return_value = {}
# Act
performance_timer.start()
result = await application_service.get_issue_details(123)
performance_timer.stop()
# Assert
assert result is not None
assert_performance_within_bounds(performance_timer.elapsed, 0.1, "issue details retrieval")
@pytest.mark.asyncio
async def test_create_issue_with_complex_labels(self, application_service, mock_issue_repository, mock_validation_service):
"""Test creating issue with complex label combinations."""
# Arrange
labels = ["bug", "priority:critical", "status:new", "frontend", "needs-investigation"]
created_issue = (IssueBuilder()
.with_number(789)
.with_title("Complex Issue")
.with_labels(*labels)
.build())
mock_issue_repository.create_issue.return_value = created_issue
# Act
result = await application_service.create_issue(
title="Complex Issue",
labels=labels
)
# Assert
assert result.number == 789
assert len(result.labels) == 5
# Verify all labels are present
label_names = {label.name for label in result.labels}
expected_labels = set(labels)
assert label_names == expected_labels
@pytest.mark.asyncio
async def test_concurrent_issue_operations(self, application_service, mock_issue_repository):
"""Test concurrent issue operations don't interfere."""
import asyncio
# Arrange
issues = [
(IssueBuilder().with_number(i).with_title(f"Issue {i}").build())
for i in range(1, 6)
]
def get_issue_side_effect(number):
return issues[number - 1]
mock_issue_repository.get_issue.side_effect = get_issue_side_effect
# Act - Simulate concurrent requests
tasks = []
for i in range(1, 6):
task = application_service.get_issue_details(i)
tasks.append(task)
results = await asyncio.gather(*tasks)
# Assert
assert len(results) == 5
for i, result in enumerate(results, 1):
assert result["issue"].number == i
assert result["issue"].title == f"Issue {i}"
@pytest.mark.asyncio
async def test_error_handling_preserves_state(self, application_service, mock_issue_repository, mock_validation_service):
"""Test that errors don't leave the application in inconsistent state."""
# Arrange - First call succeeds, second fails
success_issue = (IssueBuilder().with_number(1).with_title("Success").build())
mock_issue_repository.create_issue.side_effect = [success_issue, Exception("Database error")]
# Act - First call should succeed
result1 = await application_service.create_issue("Success Issue")
assert result1.title == "Success"
# Second call should fail but not affect future calls
with pytest.raises(Exception, match="Database error"):
await application_service.create_issue("Failing Issue")
# Third call should work if repository is fixed
mock_issue_repository.create_issue.side_effect = None
success_issue2 = (IssueBuilder().with_number(3).with_title("Recovery").build())
mock_issue_repository.create_issue.return_value = success_issue2
result3 = await application_service.create_issue("Recovery Issue")
assert result3.title == "Recovery"

View File

@@ -0,0 +1,3 @@
"""
Unit tests for infrastructure components.
"""

View File

@@ -0,0 +1,287 @@
"""
Tests to validate the testing infrastructure works correctly.
Demonstrates:
- Test fixtures functionality
- Mock factories usage
- Test builders patterns
- Custom assertions
"""
import pytest
from pathlib import Path
from datetime import datetime, timezone
from tests.fixtures.markdown_samples import MarkdownDocumentBuilder, SAMPLE_SIMPLE_DOCUMENT
from tests.fixtures.api_responses import GiteaApiResponseBuilder, SAMPLE_ISSUE_RESPONSE
from tests.utils.test_builders import IssueBuilder, LabelBuilder, create_sample_issue
from tests.utils.mock_factories import MockRepositoryFactory, MockConfigFactory
from tests.utils.assertions import (
assert_issue_equal, assert_file_exists, assert_directory_exists,
assert_performance_within_bounds, validate_issue_data
)
class TestTestingInfrastructure:
"""Test that the testing infrastructure components work correctly."""
def test_test_workspace_fixture(self, test_workspace):
"""Test that test workspace fixture creates proper isolated environment."""
# Assert
assert_directory_exists(test_workspace)
assert_directory_exists(test_workspace / "documents")
assert_directory_exists(test_workspace / "cache")
assert_directory_exists(test_workspace / "workspaces")
# Test creating files in workspace
test_file = test_workspace / "test_file.txt"
test_file.write_text("test content")
assert_file_exists(test_file)
def test_markdown_document_builder(self):
"""Test markdown document builder functionality."""
# Act
doc = (MarkdownDocumentBuilder()
.with_metadata("title", "Test Doc")
.with_metadata("author", "Test Author")
.with_heading("Main Title")
.with_paragraph("This is a test paragraph.")
.with_list(["Item 1", "Item 2", "Item 3"])
.with_code_block("print('hello')", "python")
.build())
# Assert
assert "title: Test Doc" in doc
assert "author: Test Author" in doc
assert "# Main Title" in doc
assert "This is a test paragraph." in doc
assert "- Item 1" in doc
assert "```python" in doc
assert "print('hello')" in doc
def test_gitea_api_response_builder(self):
"""Test Gitea API response builder functionality."""
# Act
response = (GiteaApiResponseBuilder()
.with_number(42)
.with_title("Test Issue")
.with_labels("bug", "priority:high")
.with_milestone("Version 1.0")
.build())
# Assert
assert response["number"] == 42
assert response["title"] == "Test Issue"
assert len(response["labels"]) == 2
assert response["labels"][0]["name"] == "bug"
assert response["labels"][1]["name"] == "priority:high"
assert response["milestone"]["title"] == "Version 1.0"
def test_issue_builder_functionality(self):
"""Test issue builder creates proper domain objects."""
# Act
issue = (IssueBuilder()
.with_number(123)
.with_title("Test Issue")
.as_bug()
.with_priority("high")
.with_status("in-progress")
.build())
# Assert
assert issue.number == 123
assert issue.title == "Test Issue"
assert len(issue.labels) == 3
# Check label categorization
categories = issue.categorize_labels()
assert "bug" in categories.type_labels
assert "priority:high" in categories.priority_labels
assert "status:in-progress" in categories.state_labels
def test_label_builder_functionality(self):
"""Test label builder creates correct labels."""
# Act
state_label = LabelBuilder().as_state_label("blocked").build()
priority_label = LabelBuilder().as_priority_label("critical").build()
type_label = LabelBuilder().as_type_label("bug").build()
custom_label = LabelBuilder().with_custom_name("frontend").build()
# Assert
assert state_label.name == "status:blocked"
assert state_label.is_state_label()
assert priority_label.name == "priority:critical"
assert priority_label.is_priority_label()
assert type_label.name == "bug"
assert type_label.is_type_label()
assert custom_label.name == "frontend"
assert not custom_label.is_state_label()
assert not custom_label.is_priority_label()
assert not custom_label.is_type_label()
def test_mock_repository_factory(self):
"""Test mock repository factory creates proper mocks."""
# Act
issue_repo = MockRepositoryFactory.create_issue_repository()
project_repo = MockRepositoryFactory.create_project_repository()
document_repo = MockRepositoryFactory.create_document_repository()
# Assert
assert hasattr(issue_repo, 'get_issue')
assert hasattr(issue_repo, 'create_issue')
assert hasattr(issue_repo, 'update_issue')
assert hasattr(issue_repo, 'list_issues')
assert hasattr(project_repo, 'get_project')
assert hasattr(project_repo, 'get_issue_project_info')
assert hasattr(document_repo, 'store_document')
assert hasattr(document_repo, 'search_content')
def test_mock_config_factory(self):
"""Test mock configuration factory."""
# Act
config = MockConfigFactory.create_test_config({
"custom_setting": "test_value",
"workspace_dir": "/custom/workspace"
})
# Assert
assert config.workspace_dir == "/custom/workspace"
assert config.custom_setting == "test_value"
assert config.gitea_url == "http://test-gitea.com" # Default value
assert config.log_level == "DEBUG" # Default value
def test_custom_assertions(self):
"""Test custom assertion functions."""
# Arrange
issue1 = create_sample_issue(1, "Test Issue")
issue2 = create_sample_issue(1, "Test Issue")
# Act & Assert - Should not raise
assert_issue_equal(issue1, issue2, ignore_timestamps=True)
# Test performance assertion
execution_time = 0.05 # 50ms
assert_performance_within_bounds(execution_time, 0.1, "test operation")
# Test data validation
valid_issue_data = {
"number": 1,
"title": "Valid Issue",
"state": "open",
"labels": [],
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
assert validate_issue_data(valid_issue_data) is True
invalid_issue_data = {
"number": -1, # Invalid number
"title": "", # Empty title
"state": "invalid_state", # Invalid state
}
assert validate_issue_data(invalid_issue_data) is False
def test_sample_constants(self):
"""Test that sample constants are properly formed."""
# Test markdown samples
assert len(SAMPLE_SIMPLE_DOCUMENT) > 0
assert "# Simple Document" in SAMPLE_SIMPLE_DOCUMENT
# Test API response samples
assert SAMPLE_ISSUE_RESPONSE["number"] == 123
assert SAMPLE_ISSUE_RESPONSE["title"] == "Sample Issue"
assert len(SAMPLE_ISSUE_RESPONSE["labels"]) > 0
def test_performance_timer_fixture(self, performance_timer):
"""Test performance timer fixture functionality."""
# Act
performance_timer.start()
# Simulate some work
import time
time.sleep(0.01) # 10ms
performance_timer.stop()
# Assert
assert performance_timer.elapsed > 0
assert performance_timer.elapsed < 0.1 # Should be much less than 100ms
def test_test_config_fixture(self, test_config):
"""Test test configuration fixture."""
# Assert
assert "workspace_dir" in test_config
assert "database_path" in test_config
assert "gitea_url" in test_config
assert test_config["gitea_url"] == "http://test-gitea.com"
def test_sample_markdown_content_fixture(self, sample_markdown_content):
"""Test sample markdown content fixture."""
# Assert
assert "Test Document" in sample_markdown_content
assert "title:" in sample_markdown_content # Front matter
assert "**bold**" in sample_markdown_content
assert "```python" in sample_markdown_content
def test_sample_issue_data_fixture(self, sample_issue_data):
"""Test sample issue data fixture."""
# Assert
assert sample_issue_data["number"] == 123
assert sample_issue_data["title"] == "Test Issue"
assert sample_issue_data["state"] == "open"
assert len(sample_issue_data["labels"]) > 0
def test_isolated_environment_fixture(self, isolated_environment):
"""Test isolated environment fixture."""
# Assert
assert "MARKITECT_WORKSPACE_DIR" in isolated_environment
assert "MARKITECT_GITEA_URL" in isolated_environment
assert isolated_environment["MARKITECT_GITEA_URL"] == "http://test-gitea.com"
@pytest.mark.parametrize("priority,expected", [
("low", "Low"),
("medium", "Medium"),
("high", "High"),
("critical", "Critical")
])
def test_parametrized_testing_works(self, priority, expected):
"""Test that parametrized testing works with our infrastructure."""
# Act
issue = (IssueBuilder()
.with_number(1)
.with_title("Priority Test")
.with_priority(priority)
.build())
# Assert
categories = issue.categorize_labels()
priority_labels = categories.priority_labels
assert len(priority_labels) == 1
assert priority_labels[0] == f"priority:{priority}"
def test_test_markers_work(self):
"""Test that our custom test markers work."""
# This test validates that the marker configuration is working
# The markers are defined in pytest.ini and should not cause warnings
pass
@pytest.mark.unit
def test_unit_marker_works(self):
"""Test that unit marker works."""
assert True
@pytest.mark.performance
def test_performance_marker_works(self, performance_timer):
"""Test that performance marker works."""
performance_timer.start()
# Simulate quick operation
result = sum(range(100))
performance_timer.stop()
assert result == 4950 # Mathematical verification
assert performance_timer.elapsed < 0.01 # Should be very fast