Testing & Quality Assurance
Running the suite
From repo root with .venv active:
.venv/bin/python -m pytest # full suite
.venv/bin/python -m pytest -m unit # unit only
.venv/bin/python -m pytest -m "not slow" # skip slow ML tests
.venv/bin/python -m pytest tests/test_basic.py -v
pytest.ini config:
testpaths = tests--strict-markers— unknown markers fail.- Filters out
transformers/torchdeprecation noise.
Markers (enforced)
| Marker | Use for |
|---|---|
unit |
Pure logic, no I/O, no network, no model load. |
integration |
Hits real DB or external services. Skipped in CI by default. |
translation |
Loads Helsinki / Ollama models. Slow + heavy. |
slow |
Anything > 5s. |
Always mark a test. Unmarked tests run in every job and slow CI down.
Fixtures (from tests/conftest.py)
| Fixture | Scope | What you get |
|---|---|---|
mock_db |
session | MagicMock shaped like DictionaryDB (collections respond to .find/.find_one/.count_documents) |
flask_app |
session | The real Flask app with DictionaryDB patched to mock_db and a guarded open() |
client |
function | flask_app.test_client() |
auth_headers |
function | Currently sets the wrong session keys — see warning below |
The mock is wired by patch("src.database.dictionary_db.DictionaryDB", return_value=mock_db) — i.e. patching the class before app is imported. Do not later try patch("app.dict_db", ...); the singleton is already the mock and re-patching at the module level fights the class-level patch.
⚠️ The auth_headers fixture is broken
It sets sess["authenticated"] and sess["user"], but the production gate checks sess["logged_in"] + sess["user_email"] + sess["session_id"] (app.py). Until the fixture is fixed, build the session inline in your test:
def test_authenticated_route(client, flask_app):
with client.session_transaction() as sess:
sess["logged_in"] = True
sess["user_email"] = "test@example.com"
sess["user_role"] = "admin"
sess["session_id"] = "test-session"
# Also bypass the single-session DB check
with patch("app.get_user_db") as gud:
gud.return_value.is_session_valid.return_value = True
resp = client.get("/api/dictionary/search?q=ran")
assert resp.status_code == 200
If you fix auth_headers, also patch is_session_valid inside it.
Configuring mock_db per test
def test_search(client, mock_db):
mock_db.search_words.return_value = [
{"chuukese_word": "ran", "english_translation": "water"}
]
# OR via raw collection:
mock_db.dictionary_collection.find.return_value = iter([
{"chuukese_word": "ran", "english_translation": "water"}
])
The methods that exist on the real DictionaryDB (and that you should mock):
search_word,search_words,add_wordsearch_phrases,add_phrase
There is no search_entries, bulk_insert_entries, etc. Don't mock methods that don't exist — the real code will never call them.
Translation tests
@pytest.mark.translation
@pytest.mark.slow
def test_helsinki_chk_to_en():
from src.translation.helsinki_translator_v2 import HelsinkiTranslator
t = HelsinkiTranslator()
t.setup_models() # no direction arg
assert t.translate("ran", "chk_to_en") == "water"
Use BLEU only when you control the reference set — small samples are noisy.
Test file inventory
The tests/ directory mixes real pytest tests and exploratory scripts. Files prefixed test_ are collected:
test_basic.py— publication manager, jworg lookup. Some assertions hit the network — markintegration.test_collections.py— DB collection ops.test_helsinki_trainer.py— fine-tuner helpers (markslow).test_translation.py— end-to-end translate (marktranslation/slow).test_scripture_parsing.py— pure unit (regex, book lookup).test_word_families.py— pure unit.
Scripts without the test_ prefix (e.g. find_complex_words.py, debug_models.py) are not test runners — they exist for one-off investigation.
CI guidance
The default CI invocation should be pytest -m "not slow and not translation and not integration" to keep runs fast. Translation/integration jobs run on a separate, slower lane.
Pitfalls
- Importing
apptriggers DB init — that's why fixtures patchDictionaryDBbefore the import. Don'timport appat module top of a test file; let the fixture do it. - The
_safe_openin conftest blocks file opens outsideconfig/andmodels/— if your test needs to read a file, point it underconfig/or extend the allowlist. - Two gunicorn workers in prod ≠ test fixture state. Don't write tests that assume cross-request in-memory state.
WTF_CSRF_ENABLED=Falseis set in the fixture, but the app does not actually use Flask-WTF — the flag is a no-op kept for legacy reasons.