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/torch deprecation 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](../../../tests/conftest.py))
| Fixture |
Scope |
What you get |
mock_db |
session |
MagicMock shaped like DictionaryDB (collections respond to .find/.findone/.countdocuments) |
flask_app |
session |
The real Flask app with DictionaryDB patched to mock_db and a guarded open() |
client |
function |
flaskapp.testclient() |
auth_headers |
function |
Currently sets the wrong session keys — see warning below |
The mock is wired by patch("src.database.dictionarydb.DictionaryDB", returnvalue=mockdb) — i.e. patching the class before app is imported. Do not later try patch("app.dictdb", ...); 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["loggedin"] + sess["useremail"] + sess["session_id"] ([app.py](../../../app.py#L442)). 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"] = "[email protected]"
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 authheaders, also patch issession_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):
searchword, searchwords, add_word
searchphrases, addphrase
There is no searchentries, bulkinsert_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 — mark integration.
test_collections.py — DB collection ops.
testhelsinkitrainer.py — fine-tuner helpers (mark slow).
test_translation.py — end-to-end translate (mark translation/slow).
testscriptureparsing.py — pure unit (regex, book lookup).
testwordfamilies.py — pure unit.
Scripts without the test prefix (e.g. findcomplexwords.py, debugmodels.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
app triggers DB init — that's why fixtures patch DictionaryDB before the import. Don't import app at module top of a test file; let the fixture do it.
- The
safeopen in conftest blocks file opens outside config/ and models/ — if your test needs to read a file, point it under config/ or extend the allowlist.
- Two gunicorn workers in prod ≠ test fixture state. Don't write tests that assume cross-request in-memory state.
WTFCSRFENABLED=False is set in the fixture, but the app does not actually use Flask-WTF — the flag is a no-op kept for legacy reasons.