Instruction file imported from yashovardhan99/niveshpy (
.github/instructions/tests.instructions.md). Copyright stays with the author.
NiveshPy Test Conventions
Tests mirror the niveshpy/ package layout. There are three distinct test layers, each with its own fixture pattern. Pick the one matching the layer you're testing — do not mix them.
1. Service tests — tests/services/**
Pure unit tests against niveshpy/services/*. Inject the Mock*Repository classes from tests/services/conftest.py; never construct a SqliteDatabase here.
from niveshpy.services.account import AccountService
from tests.services.conftest import MockAccountRepository
@pytest.fixture
def account_service() -> AccountService:
return AccountService(account_repository=MockAccountRepository())
- The mocks implement the same
domain/repositories/*Protocolas the real SQLite repos, so service code is exercised unchanged. - Group tests under
class TestMethodName:(one class per service method) — see tests/services/test_account_service.py for the canonical layout. - If you add a new method to a repository protocol, also add it to the matching
Mock*Repositoryintests/services/conftest.py, or service tests will fail at runtime.
2. Repository / infrastructure tests — tests/infrastructure/sqlite/**
Use the db fixture from tests/conftest.py, which builds a fresh in-memory SqliteDatabase(db_path=Path(":memory:")) and runs migrations:
def test_insert_account(db):
repo = SqliteAccountRepository(db)
...
- One
dbper test (function scope) — do not share state across tests. - Test SQL/row-mapping behavior here, not service logic.
3. CLI integration tests — tests/cli/**
Use the cli_scenario + runner fixtures from tests/cli/conftest.py. The cli_in_memory_db autouse fixture monkeypatches Application.db to an in-memory database, so every cli_scenario.invoke([...]) call hits a clean, isolated DB.
def test_accounts_add(cli_scenario):
account_id = cli_scenario.add_account("HDFC Savings", "HDFC Bank")
accounts = cli_scenario.invoke_json(["accounts", "list"])
assert accounts[0]["id"] == account_id
- Prefer the
CliScenariohelpers (add_account,add_security,add_transaction,invoke_json) over hand-rolling Click invocations — they assert exit codes and parse JSON output uniformly. - Pass
--no-inputto commands that prompt; the runner won't supply stdin. - Use
expected_exit_code=Noninvoke()for negative-path tests.
General Conventions
- Filesystem isolation: the session-wide autouse
mock_platformdirsfixture in tests/conftest.py redirectsplatformdirs.user_data_pathtotmp_path_factory. Never hit the real user data dir. assertis allowed intests/*(ruffS101is disabled there). Plainassert ...is preferred overunittest-style asserts.- Naming:
test_<module>.pymirroring the source module (e.g.,niveshpy/services/account.py→tests/services/test_account_service.py). - Decimal money: in fixtures, pass amounts as strings (e.g.,
"1000.50") soDecimalparsing is exercised. Never usefloat. - Exceptions: assert against the specific subclass from niveshpy/exceptions.py (e.g.,
pytest.raises(InvalidInputError)), not bareException. - Running tests:
uv run coverage run -m pytest # full suite uv run pytest tests/services/test_account_service.py::TestListAccounts uv run pytest -k "test_account_create"