> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myspellchecker.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing Guide

> This guide covers how to run and write tests for mySpellChecker.

Learn how to run the test suite, write new tests with pytest markers and fixtures, check coverage, and integrate tests into CI/CD pipelines.

## Running Tests

### All Tests

```bash theme={null}
pytest tests/
```

### With Coverage

```bash theme={null}
pytest tests/ --cov=myspellchecker --cov-report=html

# View report
open htmlcov/index.html
```

### Quick Smoke Test

```bash theme={null}
pytest tests/ -x -q --tb=short
```

### Verbose Output

```bash theme={null}
pytest tests/ -v
```

## Test Selection

### By File

```bash theme={null}
pytest tests/test_spellchecker.py
```

### By Pattern

```bash theme={null}
# Run tests containing "context"
pytest tests/ -k "context"

# Run tests NOT containing "slow"
pytest tests/ -k "not slow"
```

### By Marker

```bash theme={null}
# Only unit tests
pytest tests/ -m unit

# Only integration tests
pytest tests/ -m integration

# Skip slow tests
pytest tests/ -m "not slow"
```

### Single Test

```bash theme={null}
pytest tests/test_spellchecker.py::TestSpellChecker::test_check_valid_text
```

## Test Markers

Registered markers:

| Marker                     | Description                             |
| -------------------------- | --------------------------------------- |
| `@pytest.mark.unit`        | Fast, isolated unit tests               |
| `@pytest.mark.integration` | Tests with external dependencies        |
| `@pytest.mark.slow`        | Long-running tests (skipped by default) |
| `@pytest.mark.e2e`         | End-to-end tests                        |
| `@pytest.mark.benchmark`   | Benchmark and performance tests         |

> **Note**: These five markers are registered in `pyproject.toml`. Using unregistered markers will cause warnings with `--strict-markers`. Slow tests are skipped by default; run them explicitly with `pytest -m slow`.

```python theme={null}
import pytest

@pytest.mark.unit
def test_normalize():
    from myspellchecker.text.normalize import normalize
    assert normalize("test") == "test"

@pytest.mark.slow
@pytest.mark.integration
def test_large_corpus():
    # Long-running test
    pass
```

## Test Structure

### Directory Layout

<Tree>
  <Tree.Folder name="tests" defaultOpen>
    <Tree.File name="conftest.py" />

    <Tree.File name="test_spellchecker.py" />

    <Tree.File name="test_validators_edge_cases.py" />

    <Tree.File name="test_sqlite_provider.py" />

    <Tree.File name="test_symspell.py" />

    <Tree.File name="test_ngram_*.py" />

    <Tree.File name="test_viterbi.py" />

    <Tree.Folder name="integration" defaultOpen>
      <Tree.File name="test_config_loading.py" />

      <Tree.File name="test_cli_stdin_stdout.py" />
    </Tree.Folder>

    <Tree.Folder name="e2e" defaultOpen>
      <Tree.File name="test_cli_e2e.py" />

      <Tree.File name="test_robustness.py" />
    </Tree.Folder>

    <Tree.Folder name="fixtures" defaultOpen>
      <Tree.File name="config_templates.py" />

      <Tree.File name="myanmar_test_samples.py" />

      <Tree.Folder name="benchmarks" />
    </Tree.Folder>
  </Tree.Folder>
</Tree>

### Naming Conventions

```python theme={null}
# Test file: test_<module>.py
# Test class: Test<ClassName>
# Test function: test_<behavior>

class TestSpellChecker:
    def test_check_returns_response(self):
        pass

    def test_check_detects_invalid_syllable(self):
        pass

    def test_check_with_empty_input_returns_no_errors(self):
        pass
```

## Fixtures

### Common Fixtures (conftest.py)

```python theme={null}
import pytest
from myspellchecker import SpellChecker
from myspellchecker.providers import MemoryProvider

@pytest.fixture
def checker():
    """Create a spell checker for testing."""
    return SpellChecker()

@pytest.fixture
def memory_provider():
    """Create an in-memory provider."""
    provider = MemoryProvider()
    provider.add_word("မြန်မာ", frequency=100)
    provider.add_word("နိုင်ငံ", frequency=100)
    return provider

@pytest.fixture
def checker_with_memory(memory_provider):
    """Spell checker with memory provider."""
    return SpellChecker(provider=memory_provider)
```

### Fixture Scope

```python theme={null}
@pytest.fixture(scope="module")
def expensive_resource():
    """Created once per module."""
    return create_expensive_resource()

@pytest.fixture(scope="session")
def database():
    """Created once per test session."""
    return create_test_database()
```

## Writing Tests

### Basic Test

```python theme={null}
def test_check_valid_text(checker):
    result = checker.check("မြန်မာ")
    assert not result.has_errors

def test_check_invalid_text(checker):
    result = checker.check("invalid_text_xyz")
    assert result.has_errors
```

### Parameterized Tests

```python theme={null}
import pytest

@pytest.mark.parametrize("text,expected_errors", [
    ("မြန်မာ", 0),
    ("မြန်မာနိုင်ငံ", 0),
    ("xyz", 1),
])
def test_check_various_inputs(checker, text, expected_errors):
    result = checker.check(text)
    assert len(result.errors) == expected_errors
```

### Exception Testing

```python theme={null}
import pytest
from myspellchecker import SpellChecker
from myspellchecker.providers import SQLiteProvider
from myspellchecker.core.exceptions import MissingDatabaseError

def test_missing_database_raises():
    with pytest.raises(MissingDatabaseError):
        provider = SQLiteProvider(database_path="/nonexistent/path.db")
        SpellChecker(provider=provider)
```

### Async Tests

```python theme={null}
import pytest

@pytest.mark.asyncio
async def test_async_check(checker):
    result = await checker.check_async("မြန်မာ")
    assert not result.has_errors
```

<Note>
  `pytest-asyncio` must be installed separately (`pip install pytest-asyncio`) as it is not included in the dev dependencies.
</Note>

### Mock Testing

```python theme={null}
from unittest.mock import Mock, patch

def test_with_mocked_provider():
    mock_provider = Mock()
    mock_provider.contains_word.return_value = True

    checker = SpellChecker(provider=mock_provider)
    result = checker.check("test")

    mock_provider.contains_word.assert_called()
```

## Test Data

### Loading Test Data

```python theme={null}
import json
from pathlib import Path

@pytest.fixture
def test_samples():
    from tests.fixtures.myanmar_test_samples import VALID_WORDS
    return VALID_WORDS

def test_all_words_valid(checker, test_samples):
    for word in test_samples:
        result = checker.check(word)
        assert not result.has_errors, f"Expected valid: {word}"
```

### Myanmar Test Samples

```python theme={null}
@pytest.fixture
def myanmar_samples():
    from tests.fixtures.myanmar_test_samples import SAMPLE_SENTENCES
    return SAMPLE_SENTENCES
```

## Coverage Requirements

The project requires **≥75% code coverage**:

```bash theme={null}
# Check coverage
pytest tests/ --cov=myspellchecker --cov-fail-under=75
```

### Excluding Code from Coverage

```python theme={null}
# pragma: no cover - exclude line
if TYPE_CHECKING:  # pragma: no cover
    from typing import Optional

# Or in pyproject.toml
[tool.coverage.run]
omit = [
    "*/tests/*",
    "*/examples/*",
    "*/__pycache__/*",
]
```

## Performance Testing

### Benchmarks

```python theme={null}
import pytest

@pytest.mark.slow
def test_performance_single_check(checker, benchmark):
    result = benchmark(checker.check, "မြန်မာနိုင်ငံ")
    assert not result.has_errors

@pytest.mark.slow
def test_performance_batch(checker, benchmark):
    texts = ["မြန်မာ"] * 100
    results = benchmark(checker.check_batch, texts)
    assert len(results) == 100
```

### Running Benchmarks

```bash theme={null}
pytest tests/ -m slow --benchmark-only
```

## CI/CD Integration

### GitHub Actions Example

```yaml theme={null}
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          pip install -e ".[dev]"
          python setup.py build_ext --inplace

      - name: Run tests
        run: pytest tests/ --cov=myspellchecker --cov-fail-under=75

      - name: Upload coverage
        uses: codecov/codecov-action@v3
```

## Troubleshooting

### Tests Not Found

```bash theme={null}
# Ensure proper naming
# Files: test_*.py
# Functions: test_*
# Classes: Test*

# Check collection
pytest tests/ --collect-only
```

### Fixture Not Found

```bash theme={null}
# Check conftest.py location
# Fixtures must be in conftest.py or imported

# List available fixtures
pytest tests/ --fixtures
```

### Slow Tests

```bash theme={null}
# Profile test duration
pytest tests/ --durations=10

# Skip slow tests during development
pytest tests/ -m "not slow"
```

## See Also

* [Development Setup](/development/setup) - Environment setup
* [Contributing](/development/contributing) - Contribution guidelines
* [Naming Conventions](/development/naming-conventions) - Code style conventions
