Villa Backend SDK — Testing Guide
This document explains **how we test the SDK**, **why tests are organized the way they are**, and **how to add new tests** following the same methodology.
Test-driven development (TDD) approach
Development follows a **test-first mindset**:
1. **Define the behavior** — what should PrePaymentValidator do when isDrafting=true?
2. **Write a failing test** — assert result.valid is False and error mentions "drafting"
3. **Implement the minimum code** — add the drafting check in the pipeline
4. **Refactor safely** — tests guard against regressions
We do not require every commit to be strictly red→green (real projects rarely do), but **every module has tests written alongside or before implementation**, and **CI blocks merges when tests fail**.
Test pyramid
┌───────────────┐
│ Integration │ 4 tests — multi-service flows
│ (slow-ish) │
└───────┬───────┘
│
┌─────────────▼─────────────┐
│ Unit tests │ ~45 tests — single class/function
│ fast, isolated, mocked │
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ Schema contract │ fixtures from villaMasterSchema
│ deterministic, offline │
└───────────────────────────┘
| Layer | Location | Speed | External deps |
|---|---|---|---|
| Unit | tests/unit/ | milliseconds | none (mocked HTTP/Cognito) |
| Integration | tests/integration/ | milliseconds | none (mocked HTTP, real schema) |
| Contract | tests/unit/test_schema_contract.py | milliseconds | bundled YAML only |
We **do not** hit shop.villamarket.com in CI. All network I/O is mocked so tests are deterministic and do not require credentials.
Directory layout
tests/
├── conftest.py # shared fixtures (orders, config, HTTP mocks)
├── fixtures/ # JSON payloads from villaMasterSchema testData
│ ├── goodSample.json # valid order → must pass schema validation
│ ├── wrongType.json # orderId is integer → must fail
│ └── extraCol.json # extra field (reference fixture)
├── helpers/
│ └── fakes.py # FakeCognitoClient shared across auth tests
├── unit/ # one file per SDK module
│ ├── test_auth.py
│ ├── test_client.py
│ ├── test_cli.py
│ ├── test_config.py
│ ├── test_http.py
│ ├── test_order_service.py
│ ├── test_payments.py
│ ├── test_schema_contract.py
│ └── test_validation.py
└── integration/
└── test_checkout_flow.py # end-to-end flows with composed mocks
What each test file covers
test_auth.py — Cognito authentication
| Test | Behavior verified |
|---|---|
test_cognito_login_success | Correct AuthFlow and token parsing |
test_cognito_login_requires_client_id | Config guard before API call |
test_cognito_secret_hash_is_deterministic | HMAC helper is stable |
test_cognito_login_with_secret_hash | Secret hash included when configured |
test_cognito_login_failure | ClientError → VillaAuthError |
test_cognito_sets_bearer_token_on_http | Id token wired to HTTP layer |
**Mocking strategy:** inject FakeCognitoClient instead of calling boto3.
test_config.py — configuration
| Test | Behavior verified |
|---|---|
test_dev_payment_paths / test_prod_payment_paths | Environment switches endpoints |
test_url_for_strips_trailing_slash_and_formats_params | URL builder |
test_config_from_env | Env var loading |
test_config_from_env_rejects_invalid_environment | Invalid VILLA_ENV raises |
test_http.py — HTTP transport
| Test | Behavior verified |
|---|---|
test_http_adds_bearer_token_when_set | Authorization header |
test_http_get_json_success | Happy path JSON GET |
test_http_raises_villa_api_error_on_4xx | HTTP errors become VillaApiError |
test_http_raises_villa_api_error_on_network_failure | Connection errors wrapped |
test_http_post_form_sets_form_content_type | Payment form encoding |
test_health_check_* | Health probe semantics |
test_order_service.py — orders module
| Test | Behavior verified |
|---|---|
test_generate_order_id | Correct path and payload |
test_create_order / test_get_order | CRUD wiring |
test_list_orders_supports_* | Response shape normalization |
test_builder_roundtrip_to_schema_validator | Builder output passes schema |
test_builder_rejects_empty_product_list | Builder invariants |
test_schema_contract.py — schema contract tests
Uses **parametrized tests** with fixtures from villaMasterSchema:
@pytest.mark.parametrize(
("fixture_name", "should_pass"),
[("goodSample.json", True), ("wrongType.json", False)],
)
def test_schema_contract_fixtures(...):
...
This ensures the bundled order.yaml and our validator stay aligned with the official schema repo.
test_validation.py — pre-payment pipeline
| Test | Behavior verified |
|---|---|
test_pre_payment_validator_local_schema_only | Offline-only mode |
test_pre_payment_validator_rejects_draft_order | Business rule: no pay while drafting |
test_pre_payment_validator_runs_remote_checks | Full pipeline steps |
test_pre_payment_validator_detects_total_mismatch | Amount comparison |
test_pre_payment_validator_remote_order_exists | Optional remote fetch step |
test_api_validator_* | Payload building and verify call |
test_payments.py — payment service
| Test | Behavior verified |
|---|---|
test_create_card_token_success/failure | Token endpoint handling |
test_initiate_card_payment_extracts_redirect | HTML redirect parsing |
test_parse_callback_url | Callback query param parsing |
test_client.py — facade
| Test | Behavior verified |
|---|---|
test_villa_client_exposes_all_services | Composition root wiring |
test_villa_client_login_sets_http_bearer_token | Login → HTTP integration |
test_villa_client_from_env | Factory method |
test_cli.py — CLI smoke tests
Uses click.testing.CliRunner to invoke commands without a shell:
| Test | Behavior verified |
|---|---|
test_cli_health_command | villa health exit code and output |
test_cli_validate_schema_command | villa order validate-schema |
test_cli_parse_callback_command | villa payment parse-callback |
test_http_integration.py — wire-level HTTP (responses)
Uses the responses library to intercept **real requests calls** at the network layer. Unlike MagicMock, this verifies:
HttpClient| Test | Behavior verified |
|---|---|
test_http_order_get_uses_real_requests_stack | GET order through live requests stack |
test_http_generate_order_id_and_create | Two-step order creation flow |
test_http_pre_payment_hits_pricing_endpoints | Pricing APIs called with correct paths |
test_http_card_token_and_payment_endpoints | Dev payment endpoints |
test_http_payment_verify_endpoint | Payment verification POST |
test_cognito_integration.py — AWS Cognito (moto)
Uses moto to emulate Cognito in-process:
| Test | Behavior verified |
|---|---|
test_cognito_login_against_moto_user_pool | Full InitiateAuth against emulated pool |
test_villa_client_login_propagates_moto_token_to_http | Token flows from Cognito → HttpClient |
test_checkout_flow.py — composed business flows
Higher-level scenarios combining multiple SDK modules (validate → pay, auth → order fetch).
test_checkout_flow.py — composed business flows
| Test | Behavior verified |
|---|---|
test_checkout_flow_with_validation_before_payment | validate → pay happy path |
test_order_service_generate_and_create | generate ID → create order |
test_end_to_end_validate_then_pay_with_mismatch_blocks_payment | bad total blocks flow |
test_authenticated_order_fetch_after_login | login token used on order GET |
Shared fixtures (conftest.py)
| Fixture | Provides |
|---|---|
good_order_payload | Valid order dict from villaMasterSchema |
wrong_type_payload | Invalid orderId type |
extra_column_payload | Reference payload with extra column |
sample_order | Minimal valid Order built via OrderBuilder |
villa_config | Pre-configured VillaConfig for dev |
http_client | HttpClient with test config |
mock_http_response | Factory for fake requests.Response |
Fixtures keep tests **short and readable** — each test focuses on one behavior, not JSON loading boilerplate.
Mocking conventions
HTTP — patch session.request
with patch.object(http_client.session, "request", return_value=mock_response):
result = http_client.get_json(...)
We mock at the **session level** (not requests.post globally) so each HttpClient instance is isolated.
Cognito — inject fake client
service = CognitoAuthService(config, client=FakeCognitoClient())
Constructor injection avoids patching boto3 and keeps auth tests pure unit tests.
API validator in integration tests — replace on client
client.api_validator = MagicMock()
client.pre_payment = PrePaymentValidator(client.schema_validator, client.api_validator)
When testing composed flows, replace the **validator collaborator** rather than patching deep internals.
Markers
Tests use pytest markers defined in pyproject.toml:
@pytest.mark.unit
def test_something(): ...
@pytest.mark.integration
def test_checkout_flow(): ...
Run selectively:
pytest -m unit
pytest -m integration
Running tests locally
# Install with dev dependencies
pip install -e ".[dev]"
# Full suite
pytest
# Verbose with coverage
pytest -v --cov=villa_backend_sdk --cov-report=term-missing
# Single file
pytest tests/unit/test_validation.py -v
# Single test
pytest tests/unit/test_validation.py::test_pre_payment_validator_rejects_draft_order -v
Expected result: **all 67 tests pass** with no live backend access required.
CI pipeline
GitHub Actions (.github/workflows/ci.yml) runs on every push and pull request:
Python 3.10 ──┐
Python 3.11 ──┼── pip install -e ".[dev]"
Python 3.12 ──┘
│
├── pytest tests/unit -v --cov
├── pytest tests/integration -v
├── pytest -v (full suite)
└── python -m build (package still builds)
CI uses the same commands as local development — **if it passes locally, it should pass in CI**.
Adding a new test (checklist)
When you add a feature, follow this checklist:
1. **Pick the layer**
- Single function/class change → tests/unit/test_<module>.py
- Multiple services interacting → tests/integration/
2. **Name the test after behavior**
- Good: test_pre_payment_validator_rejects_draft_order
- Avoid: test_validator_1
3. **Arrange → Act → Assert**
```python
# Arrange
validator = PrePaymentValidator(...)
draft_order = order.model_copy(update={"is_drafting": True})
# Act
result = validator.validate(draft_order, verify_cost=False, verify_grand_total=False)
# Assert
assert result.valid is False
assert "drafting" in result.errors[0]
```
4. **Mock external I/O** — never call live APIs in unit/integration tests
5. **Run the suite**
```bash
pytest -v
```
6. **If adding schema fields** — update fixtures or add new fixture JSON from villaMasterSchema testData/
Schema fixture policy
Fixtures in tests/fixtures/ come from villaMasterSchema/order/testData:
| Fixture | Expected validation result | Reason |
|---|---|---|
goodSample.json | Pass | Official valid sample |
wrongType.json | Fail | orderId is integer, not string |
extraCol.json | Reference only | Documents extra-field behavior |
When villaMasterSchema changes, **update bundled schemas and fixtures together**, then rerun contract tests.
Coverage expectations
We aim for high coverage on **service logic**, not boilerplate:
| Module | Priority |
|---|---|
validation/pre_payment.py | Critical — guards payment |
auth/service.py | Critical — security |
payments/service.py | Critical — money |
orders/builder.py | High — data contract |
cli/main.py | Smoke tests only — thin wrapper |
Run coverage report:
pytest --cov=villa_backend_sdk --cov-report=term-missing
Uncovered lines in CLI argument parsing or __init__.py re-exports are acceptable. Uncovered lines in validation or payment paths are not.
CI/CD
See **CI_CD.md** for the full pipeline documentation.
Quick summary — CI runs on every push/PR:
| Job | Command |
|---|---|
| Lint | ruff check + ruff format --check |
| Unit | pytest tests/unit -m unit (Python 3.10–3.12) |
| Integration | pytest tests/integration -m integration |
| Build | full suite + --cov-fail-under=80 + python -m build |