# NoDupeLabs Test Suite This directory contains the test suite for NoDupeLabs, using pytest as the testing framework. ## Test Structure ```text tests/ ├──__init__.py # Package initializer ├── conftest.py # Test configuration and fixtures ├── test_basic.py # Basic functionality tests ├── run_tests.py # Test runner script ├── core/ # Core module tests ├── integration/ # Integration tests └── plugins/ # Plugin tests ``` ## Running Tests ### Using pytest directly ```bash # Run all tests python -m pytest # Run specific test file python -m pytest tests/test_basic.py # Run with verbose output python -m pytest -v # Run with coverage python -m pytest --cov=nodupe ``` ## Using the test runner ```bash ## Run all tests python tests/run_tests.py ## Run specific test file python tests/run_tests.py tests/test_basic.py ## Run with verbose output python tests/run_tests.py -v # Run specific test markers python tests/run_tests.py --unit python tests/run_tests.py --integration python tests/run_tests.py --slow ``` ## Test Markers The following markers are available for test selection: - `unit`: Unit tests - `integration`: Integration tests - `slow`: Slow-running tests - `e2e`: End-to-end tests ## Fixtures ### Available Fixtures - `temp_dir`: Creates a temporary directory for testing - `sample_files`: Creates sample files for duplicate detection testing - `mock_config`: Provides mock configuration data - `database_connection`: Provides in-memory SQLite database connection - `mock_plugin`: Creates mock plugin for plugin system testing ### Using Fixtures ```python def test_example(temp_dir): """Test example using temp_dir fixture.""" # temp_dir is a Path object pointing to a temporary directory test_file = temp_dir / "test.txt" test_file.write_text("test content") assert test_file.exists() assert test_file.read_text() == "test content" def test_database_operations(database_connection): """Test database operations using database_connection fixture.""" cursor = database_connection.cursor() cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)") cursor.execute("INSERT INTO test (name) VALUES (?)", ("test",)) cursor.execute("SELECT * FROM test") result = cursor.fetchone() assert result[1] == "test" ``` ## Test Best Practices ### Robust Test Structure ```python import pytest from pathlib import Path from nodupe.core.filesystem import safe_write, atomic_write class TestFilesystemOperations: """Test suite for filesystem operations.""" def test_safe_write_success(self, temp_dir): """Test successful safe file write operation.""" test_file = temp_dir / "test.txt" content = "Hello, World!" # Execute safe_write(test_file, content) # Verify assert test_file.exists() assert test_file.read_text() == content assert test_file.stat().st_size == len(content) def test_safe_write_permission_error(self, temp_dir, mocker): """Test safe write handles permission errors gracefully.""" test_file = temp_dir / "restricted.txt" content = "Should fail" # Mock permission error mocker.patch('builtins.open', side_effect=PermissionError("No permission")) # Execute and verify exception with pytest.raises(PermissionError): safe_write(test_file, content) def test_atomic_write_rollback(self, temp_dir, mocker): """Test atomic write rolls back on failure.""" test_file = temp_dir / "atomic.txt" temp_file = temp_dir / f".{test_file.name}.tmp" # Mock failure during write def mock_write_side_effect(*args, **kwargs): if "atomic.txt.tmp" in str(args[0]): raise IOError("Simulated failure") return original_open(*args, **kwargs) original_open = builtins.open mocker.patch('builtins.open', side_effect=mock_write_side_effect) # Execute with pytest.raises(IOError): atomic_write(test_file, "content") # Verify no files created assert not test_file.exists() assert not temp_file.exists() ``` ### Test Coverage Patterns ```python def test_happy_path(): """Test normal successful execution path.""" result = function_under_test("valid_input") assert result == expected_output def test_edge_cases(): """Test boundary conditions and edge cases.""" # Empty input result = function_under_test("") assert result == default_value # Maximum allowed input max_input = "x" * MAX_LENGTH result = function_under_test(max_input) assert len(result) == MAX_LENGTH def test_error_conditions(): """Test error handling and exception cases.""" # Invalid input type with pytest.raises(TypeError): function_under_test(123) # None input with pytest.raises(ValueError): function_under_test(None) def test_performance_constraints(): """Test performance characteristics.""" import time # Test execution time start_time = time.time() result = function_under_test(large_input) duration = time.time() - start_time assert duration < MAX_ALLOWED_TIME assert result == expected_output ``` ## Test Organization ### Recommended Test Structure ``` tests/ ├── __init__.py ├── conftest.py # Global fixtures and configuration ├── test_basic.py # Basic functionality tests ├── run_tests.py # Test runner script ├── core/ │ ├── __init__.py │ ├── conftest.py # Core-specific fixtures │ ├── test_database.py # Database tests │ ├── test_filesystem.py # Filesystem tests │ ├── test_config.py # Configuration tests │ └── test_plugins.py # Plugin system tests ├── integration/ │ ├── __init__.py │ ├── conftest.py # Integration test fixtures │ ├── test_workflows.py # End-to-end workflows │ └── test_cli.py # CLI integration tests └── plugins/ ├── __init__.py ├── conftest.py # Plugin test fixtures ├── test_commands.py # Command plugin tests └── test_similarity.py # Similarity plugin tests ``` ## Configuration Test configuration is defined in `pyproject.toml` under `[tool.pytest.ini_options]`. ### Key Settings: - **Test files**: `test_*.py` - All test files follow this pattern - **Test functions**: `test_*` - All test functions start with `test_` - **Test classes**: `Test*` - All test classes start with `Test` - **Coverage threshold**: 80% minimum line coverage - **Branch coverage**: Enabled for comprehensive testing - **Parallel execution**: Automatic based on CPU cores - **Markers**: Support for unit, integration, performance, and stress tests ### Advanced Configuration: ```toml [tool.pytest.ini_options] addopts = """ --cov=nodupe --cov-report=term-missing --cov-report=html:htmlcov --cov-report=xml:coverage.xml --cov-fail-under=80 --cov-branch -n auto -v --tb=short --durations=20 --color=yes --junitxml=test_results.xml """ ``` ## Test Quality Metrics ### Coverage Requirements - **Minimum line coverage**: 80% for all new code - **Minimum branch coverage**: 70% for all new code - **Core modules**: 90%+ coverage target - **Critical paths**: 100% coverage required - **Final Goal**: 100% unit test coverage for all code paths ### Ultimate Coverage Targets | Coverage Type | Current | Phase 1 | Phase 2 | Phase 3 | Final | |---------------|---------|---------|---------|---------|-------| | **Line Coverage** | ~31% | >60% | >80% | >90% | **100%** | | **Branch Coverage** | ~31% | >50% | >70% | >85% | **100%** | | **Unit Tests** | ~31% | >60% | >80% | >95% | **100%** | | **Integration Tests** | ~10% | >30% | >50% | >70% | >80% | | **E2E Tests** | ~5% | >15% | >30% | >50% | >60% | ### Test Types and Distribution | Test Type | Coverage Target | Purpose | |-----------|----------------|---------| | Unit Tests | 80-90% | Test individual functions and classes | | Integration Tests | 70-80% | Test module interactions | | End-to-End Tests | 60-70% | Test complete workflows | | Performance Tests | N/A | Test execution speed and resource usage | | Stress Tests | N/A | Test system under heavy load | ## Continuous Integration ### CI/CD Pipeline Configuration The project uses GitHub Actions for automated testing with the following workflow: ```yaml name: CI/CD Pipeline on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-dev.txt - name: Run tests with coverage run: | pytest --cov=nodupe --cov-report=xml --cov-fail-under=80 - name: Upload coverage uses: codecov/codecov-action@v3 ``` ### Quality Gates All pull requests must pass the following quality gates: 1. **All tests pass**: No test failures allowed 2. **Coverage maintained**: Coverage must not decrease 3. **Minimum coverage**: 80% line coverage required 4. **Type checking**: MyPy must pass with no errors 5. **Code style**: Pylint score must be 10.0/10.0 6. **Documentation**: All new features must be documented ## Test Development Workflow ### Writing New Tests 1. **Identify test cases**: Determine what needs to be tested 2. **Write test first**: Follow TDD approach when possible 3. **Implement functionality**: Write code to make tests pass 4. **Add edge cases**: Test boundary conditions and error cases 5. **Verify coverage**: Ensure adequate test coverage 6. **Update documentation**: Document new test cases ### Test Review Checklist - [ ] Tests follow naming conventions (`test_*` functions) - [ ] Tests are properly categorized (unit/integration/e2e) - [ ] Tests cover happy path, edge cases, and error conditions - [ ] Tests use appropriate fixtures and mocks - [ ] Tests are independent and isolated - [ ] Tests have clear, descriptive docstrings - [ ] Tests maintain or improve coverage - [ ] Tests run efficiently (< 1s for unit tests) - [ ] Tests follow project coding standards ## Performance Testing ### Benchmark Tests ```python import pytest import time from nodupe.core.scan import FileWalker def test_file_walker_performance(benchmark, temp_dir): """Benchmark file walker performance.""" # Create test directory structure create_large_directory_structure(temp_dir, 1000, 3) # Benchmark the walker walker = FileWalker(str(temp_dir)) result = benchmark(walker.walk) # Verify results assert len(result) == 1000 assert benchmark.extra_info['min_rounds'] >= 5 def test_hashing_throughput(benchmark, temp_dir): """Benchmark hashing throughput.""" # Create test files test_files = create_test_files(temp_dir, 100, 1024) # Benchmark hashing def hash_all_files(): from nodupe.core.scan import FileHasher hasher = FileHasher() for file in test_files: hasher.hash_file(file) result = benchmark(hash_all_files) # Verify throughput (files/second) files_per_second = 100 / result assert files_per_second > 50 # Minimum 50 files/second ``` ### Stress Testing ```python def test_memory_usage_under_load(): """Test memory usage with large datasets.""" import tracemalloc tracemalloc.start() # Process large dataset process_large_dataset(10000) # Check memory usage current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() assert peak < MAX_MEMORY_USAGE assert current < MAX_MEMORY_USAGE / 2 def test_concurrent_operations(): """Test thread safety and concurrency.""" from concurrent.futures import ThreadPoolExecutor import threading # Test with multiple threads with ThreadPoolExecutor(max_workers=8) as executor: futures = [executor.submit(process_file, f) for f in test_files] results = [f.result() for f in futures] # Verify all operations completed successfully assert all(r.success for r in results) assert len(results) == len(test_files) ``` ## Test Maintenance ### Updating Existing Tests 1. **Review test coverage**: Identify gaps in existing tests 2. **Add missing test cases**: Cover new functionality and edge cases 3. **Refactor tests**: Improve test structure and readability 4. **Update fixtures**: Enhance test fixtures as needed 5. **Verify coverage**: Ensure coverage meets requirements 6. **Document changes**: Update test documentation ### Test Refactoring Patterns ```python # Before: Duplicated test setup def test_function_a(): setup = create_complex_setup() result = function_a(setup) assert result == expected_a def test_function_b(): setup = create_complex_setup() # Duplicated result = function_b(setup) assert result == expected_b # After: Using fixtures @pytest.fixture def complex_setup(): return create_complex_setup() def test_function_a(complex_setup): result = function_a(complex_setup) assert result == expected_a def test_function_b(complex_setup): result = function_b(complex_setup) assert result == expected_b ``` ## Resources ### Test Utilities - **pytest**: Primary testing framework - **pytest-cov**: Coverage reporting - **pytest-benchmark**: Performance benchmarking - **pytest-mock**: Mocking support - **pytest-xdist**: Parallel test execution - **hypothesis**: Property-based testing ### Learning Resources - [pytest Documentation](https://docs.pytest.org/) - [Python Testing with pytest](https://pragprog.com/titles/bopytest/) - [Test-Driven Development with Python](https://www.obeythetestinggoat.com/) - [Python Mocking and Patching](https://docs.python.org/3/library/unittest.mock.html) ### Best Practices - Follow the **Arrange-Act-Assert** pattern - Keep tests **fast, isolated, repeatable, self-validating, and timely** - Test **behavior, not implementation** - Use **descriptive test names** that explain what's being tested - Keep tests **small and focused** on single responsibilities - Avoid **test interdependencies** - Use **fixtures** for complex setup and teardown - **Mock external dependencies** to ensure test isolation - **Test edge cases and error conditions** - **Document test assumptions and requirements**