-
Notifications
You must be signed in to change notification settings - Fork 0
Local Development and Testing
Everything below runs inside .venv, managed by the Makefile - make
creates it on first use and only reinstalls dependencies when
requirements*.txt actually change, so repeat runs are fast. Run
make help for the full list of targets.
make test # unit + moto-backed integration tests
make lint # ruff
make format-check # black --check
make check # all three - what CI runsIf you'd rather manage the virtual environment yourself instead of going
through make:
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt...then run pytest / ruff check . / black --check . directly from
inside it.
tests/
conftest.py # moto fixtures, make_*_item() builders, make_http_event(), import_lambda_app()
table_defs.py # hand-maintained mirror of template.yaml's DynamoDB tables
unit/ # fast, no moto
integration/
test_schema_matches_template.py # fails the build if table_defs.py drifts from template.yaml
pytest # everything
pytest tests/unit # fast, no moto
pytest tests/integration # moto-backed, exercises real Lambda handlers end-to-end
ruff check . && black --check .Notable conventions:
-
tests/conftest.py'simport_lambda_app(function_dir, module_name)loadssrc/<function_dir>/app.pyby explicit file path under a uniquesys.modulesname. Both Lambdas ship a top-levelapp.py(required by SAM), so a plainimport appwould collide between them if both were ever imported normally in the same test session. - Turnstile verification calls a real third-party HTTP endpoint that moto
can't mock - integration tests stub it with
monkeypatch.setattr("nmailx_common.turnstile.verify_turnstile_token", ...). - If you add or change a DynamoDB table in
template.yaml, updatetests/table_defs.pyto match -tests/integration/test_schema_matches_template.pywill fail the build otherwise. That's the point: it's the drift guard between the hand-maintained test fixtures and the real CloudFormation source of truth. -
db.reset_cache()/ses_client.reset_cache()exist because both moduleslru_cachetheir boto3 client/resource for warm-Lambda-container reuse; theawsautouse fixture inconftest.pyclears them between tests so a cached client from a previous test's (now torn-down) moto mock doesn't leak into the next one.
sam local start-api emulates API Gateway + Lambda locally (via a local
Docker container for the Lambda runtime itself - the deployed app remains
container-free; this is dev tooling only). Pair it with DynamoDB Local's
downloadable jar (no Docker needed for that piece - grab
DynamoDBLocal.jar from the
official download):
make local-db # java -jar DynamoDBLocal.jar -sharedDb -inMemory, in one terminal
make local-db-tables # creates the 5 tables against it
make local-api # sam local start-api, wired to DynamoDB Local, in another terminalThis loop never makes real AWS or network calls - pytest similarly never
does (moto + a stubbed Turnstile client), so the full automated test suite
is safe to run anywhere, including CI, without AWS credentials.
.github/workflows/ci.yml runs lint + tests (make check) on every PR -
no AWS credentials needed. .github/workflows/deploy.yml deploys on every
push to main via GitHub OIDC (no long-lived AWS keys in GitHub Secrets).
See the README's CI/CD section
for one-time OIDC bootstrap instructions.
-
make venvseems to hang or reinstall every time - check thatrequirements.txt/requirements-dev.txttimestamps aren't being touched by something else (e.g. a formatter or editor); the Makefile compares against a stamp file to decide whether to reinstall. -
Integration tests fail with real AWS errors (e.g.
NoCredentialsError, region errors) instead of moto behavior - make sure theawsautouse fixture inconftest.pyis actually running (it should be picked up automatically for any test intests/integration); a fixture-scoping mistake is the most common cause of moto not intercepting calls. -
Two Lambda
app.pyfiles stepping on each other in the same pytest run - useimport_lambda_app()fromconftest.pyrather than a plainimport appif you're writing a new test that needs to load one of the Lambda entry points directly.