---
tags: [testing, mocks, fakes, pytest, async, no-db]
modules: [tests/]
applies_to: [tests]
confidence: inferred
---
# Pattern: Mock-Based Testing

<!-- vibeflow:auto:start -->
## What
All tests run without a database. LLM and embedding providers are replaced with deterministic fakes. Tests exercise pipeline logic, parsing, scoring, and error handling using canned JSON responses.

## Where
- `tests/conftest.py` — Shared fakes and helpers
- `tests/test_*.py` — Test modules (flat structure)

## The Pattern
1. **FakeLLMProvider**: Takes a list of canned JSON strings. Returns them in order; repeats last when exhausted.
2. **FakeEmbeddingProvider**: Returns deterministic zero vectors (`[0.0] * 1536`).
3. **FailingEmbeddingProvider**: Returns empty/None to test degradation paths.
4. **make_fact()**: Factory that returns `MagicMock(spec=MemoryFact)` with sensible defaults. Override any field via kwargs.
5. **run_async()**: Wraps `asyncio.run()` for calling async functions from sync test methods.
6. **Test structure**: Class-based grouping (`class TestExtract:`), method per scenario.
7. **No fixtures for DB**: Tests import `conftest` fakes directly (not via pytest fixtures).

## Rules
- No DB required for any test. If a test needs DB interaction, mock the session.
- Fake providers must satisfy the Protocol interface exactly.
- Use `FakeLLMProvider([json_str1, json_str2])` to control multi-call sequences.
- Test file naming: `test_<module>.py`.
- Imports from `conftest` are direct: `from conftest import FakeLLMProvider, run_async`.
- JSON responses for extraction tests use `_make_extraction_response()` helper.

## Examples from this codebase
File: tests/conftest.py
```python
class FakeLLMProvider:
    def __init__(self, responses: list[str] | None = None) -> None:
        self._responses = list(responses or [])
        self._call_count = 0

    async def complete(self, messages, temperature=0, response_format=None, max_tokens=None) -> str:
        if self._responses:
            resp = self._responses[min(self._call_count, len(self._responses) - 1)]
        else:
            resp = '{"entities": [], "facts": [], "relations": []}'
        self._call_count += 1
        return resp

def make_fact(**overrides) -> MagicMock:
    defaults = {"id": uuid.uuid4(), "user_id": "test-user", "fact_text": "Test Entity likes coffee", ...}
    defaults.update(overrides)
    fact = MagicMock(spec=MemoryFact)
    for key, value in defaults.items():
        setattr(fact, key, value)
    return fact
```

File: tests/test_write_pipeline.py
```python
class TestExtract:
    def test_extract_parses_entities_and_facts(self):
        from arandu.write.extract import extract_from_message
        response = _make_extraction_response(
            entities=[{"name": "user", "type": "person"}],
            facts=[{"subject": "user", "fact": "Lives in SP", "confidence": 0.95}],
        )
        llm = FakeLLMProvider([response])
        result = run_async(extract_from_message("Eu moro em SP", llm=llm))
        assert len(result.facts) == 1
```
<!-- vibeflow:auto:end -->
