Files
markitect-main/tests/conftest.py
tegwick 38d9c5ca80 feat: improve async testing infrastructure and fix coroutine warnings (issue #84)
## Key Improvements:

### Enhanced Test Configuration
- Add pytest-asyncio with auto mode for better async test support
- Remove manual event loop fixture in favor of pytest-asyncio management
- Configure proper asyncio mode in pytest.ini

### New Async Test Utilities
- Add AsyncTestCase base class for automatic mock cleanup
- Add create_async_mock_that_returns/raises helper functions
- Add cleanup_async_mocks function to prevent resource warnings
- Add async_cleanup fixture for test-scoped mock management

### Fixed Coroutine Warnings
- Update TestGiteaPluginListIssues to inherit from AsyncTestCase
- Replace problematic AsyncMock usage with managed async mocks
- Mock async methods directly on plugin instances to avoid creating real coroutines
- Significantly reduced coroutine warnings in test_issue_59_gitea_plugin.py

### Results
- Reduced coroutine warnings from 11+ to ~3 remaining (75%+ improvement)
- All existing tests continue to pass
- Better async test patterns established for future development
- Proper resource cleanup prevents memory leaks in test runs

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 02:33:48 +02:00

333 lines
9.0 KiB
Python

"""
Global test configuration and fixtures for MarkiTect project.
Provides shared fixtures, utilities, and configuration for all test types.
"""
import pytest
import tempfile
import shutil
import asyncio
from pathlib import Path
from unittest.mock import Mock, AsyncMock
from typing import Generator, Dict, Any
import sqlite3
import os
# Import async test utilities
from tests.utils.assertions import cleanup_async_mocks, create_async_mock_that_returns
# Note: event_loop fixture is now handled by pytest-asyncio with asyncio_mode=auto
# This replaces the manual event loop management for better async test support
@pytest.fixture(scope="session")
def test_workspace() -> Generator[Path, None, None]:
"""Create isolated test workspace for file operations."""
temp_dir = tempfile.mkdtemp(prefix="markitect_test_")
workspace_path = Path(temp_dir)
# Create subdirectories
(workspace_path / "documents").mkdir()
(workspace_path / "cache").mkdir()
(workspace_path / "workspaces").mkdir()
yield workspace_path
# Cleanup
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.fixture
def test_database_path(test_workspace) -> Path:
"""Provide path for test database."""
return test_workspace / "test.db"
@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 = []
mock_cursor.lastrowid = 1
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_response.text = '{"status": "success"}'
mock_client.get.return_value = mock_response
mock_client.post.return_value = mock_response
mock_client.put.return_value = mock_response
mock_client.delete.return_value = mock_response
return mock_client
@pytest.fixture
def mock_async_http_client():
"""Provide mocked async HTTP client for API tests."""
mock_client = AsyncMock()
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value={"status": "success"})
mock_response.text = AsyncMock(return_value='{"status": "success"}')
mock_client.get.return_value = mock_response
mock_client.post.return_value = mock_response
mock_client.put.return_value = mock_response
mock_client.delete.return_value = mock_response
return mock_client
@pytest.fixture
def test_config(test_workspace) -> Dict[str, Any]:
"""Provide test configuration dictionary."""
return {
"workspace_dir": str(test_workspace / "workspaces"),
"database_path": str(test_workspace / "test.db"),
"cache_dir": str(test_workspace / "cache"),
"gitea_url": "http://test-gitea.com",
"gitea_token": "test-token",
"repo_owner": "test",
"repo_name": "repo",
"log_level": "DEBUG"
}
@pytest.fixture
def clean_environment():
"""Provide clean environment variables for testing."""
original_env = dict(os.environ)
# Clear relevant environment variables
test_env_vars = [
"MARKITECT_WORKSPACE_DIR",
"MARKITECT_GITEA_URL",
"MARKITECT_GITEA_TOKEN",
"MARKITECT_REPO_OWNER",
"MARKITECT_REPO_NAME"
]
for var in test_env_vars:
os.environ.pop(var, None)
yield
# Restore original environment
os.environ.clear()
os.environ.update(original_env)
@pytest.fixture
def isolated_environment(test_workspace, clean_environment):
"""Set up isolated environment for CLI testing."""
env = {
"MARKITECT_WORKSPACE_DIR": str(test_workspace / "workspaces"),
"MARKITECT_GITEA_URL": "http://test-gitea.com",
"MARKITECT_GITEA_TOKEN": "test-token",
"MARKITECT_REPO_OWNER": "test",
"MARKITECT_REPO_NAME": "repo",
"PYTHONPATH": "."
}
# Update current process environment
for key, value in env.items():
os.environ[key] = value
yield env
@pytest.fixture
def sample_markdown_content():
"""Provide sample markdown content for testing."""
return """---
title: Test Document
author: Test Author
tags: [test, sample]
---
# Test Document
This is a test document with **bold** and *italic* text.
## Section 1
- Item 1
- Item 2
- Item 3
## Section 2
Here's a code block:
```python
def hello_world():
print("Hello, World!")
```
And a link: [Test Link](https://example.com)
"""
@pytest.fixture
def sample_issue_data():
"""Provide sample issue data for testing."""
return {
"number": 123,
"title": "Test Issue",
"body": "This is a test issue description",
"state": "open",
"labels": [
{"name": "bug"},
{"name": "priority:high"},
{"name": "status:in-progress"}
],
"milestone": {
"id": 1,
"title": "Version 1.0",
"description": "First release"
},
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T12:00:00Z"
}
@pytest.fixture
def sample_project_data():
"""Provide sample project data for testing."""
return {
"name": "Test Project",
"description": "A test project for testing",
"state": "active",
"milestones": [
{
"id": 1,
"title": "Version 1.0",
"description": "First release",
"due_date": "2025-12-31T23:59:59Z",
"state": "open",
"open_issues": 5,
"closed_issues": 3
}
],
"kanban_columns": ["Todo", "In Progress", "Review", "Done"],
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
# Performance testing fixtures
@pytest.fixture
def performance_timer():
"""Timer fixture for performance testing."""
import time
class Timer:
def __init__(self):
self.start_time = None
self.end_time = None
def start(self):
self.start_time = time.time()
def stop(self):
self.end_time = time.time()
@property
def elapsed(self) -> float:
if self.start_time is None:
raise ValueError("Timer not started")
if self.end_time is None:
return time.time() - self.start_time
return self.end_time - self.start_time
return Timer()
# Async test helpers
@pytest.fixture
def async_test_timeout():
"""Default timeout for async tests."""
return 30.0 # 30 seconds
@pytest.fixture
def async_cleanup():
"""Fixture to help with async test cleanup and prevent coroutine warnings."""
mocks_to_cleanup = []
def register_mock(mock):
"""Register a mock for cleanup."""
mocks_to_cleanup.append(mock)
return mock
yield register_mock
# Cleanup all registered mocks
cleanup_async_mocks(*mocks_to_cleanup)
@pytest.fixture
def async_mock_client(async_cleanup):
"""Provide a properly configured async HTTP client mock."""
mock_client = AsyncMock()
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = create_async_mock_that_returns({"status": "success"})
mock_response.text = create_async_mock_that_returns('{"status": "success"}')
# Configure the mock client methods
mock_client.get = create_async_mock_that_returns(mock_response)
mock_client.post = create_async_mock_that_returns(mock_response)
mock_client.put = create_async_mock_that_returns(mock_response)
mock_client.delete = create_async_mock_that_returns(mock_response)
# Register for cleanup
async_cleanup(mock_client)
async_cleanup(mock_response)
return mock_client
# Test markers configuration
def pytest_configure(config):
"""Configure pytest markers."""
config.addinivalue_line(
"markers", "slow: marks tests as slow (deselect with '-m \"not slow\"')"
)
config.addinivalue_line(
"markers", "integration: marks tests as integration tests"
)
config.addinivalue_line(
"markers", "e2e: marks tests as end-to-end tests"
)
config.addinivalue_line(
"markers", "performance: marks tests as performance tests"
)
config.addinivalue_line(
"markers", "unit: marks tests as unit tests"
)
# Collection hooks
def pytest_collection_modifyitems(config, items):
"""Modify test collection to add markers based on test location."""
for item in items:
# Add markers based on test file location
if "unit" in str(item.fspath):
item.add_marker(pytest.mark.unit)
elif "integration" in str(item.fspath):
item.add_marker(pytest.mark.integration)
elif "e2e" in str(item.fspath):
item.add_marker(pytest.mark.e2e)
elif "performance" in str(item.fspath):
item.add_marker(pytest.mark.performance)