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    │
              └───────────────────────────┘
LayerLocationSpeedExternal deps
Unittests/unit/millisecondsnone (mocked HTTP/Cognito)
Integrationtests/integration/millisecondsnone (mocked HTTP, real schema)
Contracttests/unit/test_schema_contract.pymillisecondsbundled 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

TestBehavior verified
test_cognito_login_successCorrect AuthFlow and token parsing
test_cognito_login_requires_client_idConfig guard before API call
test_cognito_secret_hash_is_deterministicHMAC helper is stable
test_cognito_login_with_secret_hashSecret hash included when configured
test_cognito_login_failureClientErrorVillaAuthError
test_cognito_sets_bearer_token_on_httpId token wired to HTTP layer

**Mocking strategy:** inject FakeCognitoClient instead of calling boto3.

test_config.py — configuration

TestBehavior verified
test_dev_payment_paths / test_prod_payment_pathsEnvironment switches endpoints
test_url_for_strips_trailing_slash_and_formats_paramsURL builder
test_config_from_envEnv var loading
test_config_from_env_rejects_invalid_environmentInvalid VILLA_ENV raises

test_http.py — HTTP transport

TestBehavior verified
test_http_adds_bearer_token_when_setAuthorization header
test_http_get_json_successHappy path JSON GET
test_http_raises_villa_api_error_on_4xxHTTP errors become VillaApiError
test_http_raises_villa_api_error_on_network_failureConnection errors wrapped
test_http_post_form_sets_form_content_typePayment form encoding
test_health_check_*Health probe semantics

test_order_service.py — orders module

TestBehavior verified
test_generate_order_idCorrect path and payload
test_create_order / test_get_orderCRUD wiring
test_list_orders_supports_*Response shape normalization
test_builder_roundtrip_to_schema_validatorBuilder output passes schema
test_builder_rejects_empty_product_listBuilder 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

TestBehavior verified
test_pre_payment_validator_local_schema_onlyOffline-only mode
test_pre_payment_validator_rejects_draft_orderBusiness rule: no pay while drafting
test_pre_payment_validator_runs_remote_checksFull pipeline steps
test_pre_payment_validator_detects_total_mismatchAmount comparison
test_pre_payment_validator_remote_order_existsOptional remote fetch step
test_api_validator_*Payload building and verify call

test_payments.py — payment service

TestBehavior verified
test_create_card_token_success/failureToken endpoint handling
test_initiate_card_payment_extracts_redirectHTML redirect parsing
test_parse_callback_urlCallback query param parsing

test_client.py — facade

TestBehavior verified
test_villa_client_exposes_all_servicesComposition root wiring
test_villa_client_login_sets_http_bearer_tokenLogin → HTTP integration
test_villa_client_from_envFactory method

test_cli.py — CLI smoke tests

Uses click.testing.CliRunner to invoke commands without a shell:

TestBehavior verified
test_cli_health_commandvilla health exit code and output
test_cli_validate_schema_commandvilla order validate-schema
test_cli_parse_callback_commandvilla 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:

  • Correct URLs and HTTP methods
  • Headers (Accept, Authorization, Content-Type)
  • Full request/response cycle through HttpClient
  • TestBehavior verified
    test_http_order_get_uses_real_requests_stackGET order through live requests stack
    test_http_generate_order_id_and_createTwo-step order creation flow
    test_http_pre_payment_hits_pricing_endpointsPricing APIs called with correct paths
    test_http_card_token_and_payment_endpointsDev payment endpoints
    test_http_payment_verify_endpointPayment verification POST

    test_cognito_integration.py — AWS Cognito (moto)

    Uses moto to emulate Cognito in-process:

    TestBehavior verified
    test_cognito_login_against_moto_user_poolFull InitiateAuth against emulated pool
    test_villa_client_login_propagates_moto_token_to_httpToken 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

    TestBehavior verified
    test_checkout_flow_with_validation_before_paymentvalidate → pay happy path
    test_order_service_generate_and_creategenerate ID → create order
    test_end_to_end_validate_then_pay_with_mismatch_blocks_paymentbad total blocks flow
    test_authenticated_order_fetch_after_loginlogin token used on order GET

    Shared fixtures (conftest.py)

    FixtureProvides
    good_order_payloadValid order dict from villaMasterSchema
    wrong_type_payloadInvalid orderId type
    extra_column_payloadReference payload with extra column
    sample_orderMinimal valid Order built via OrderBuilder
    villa_configPre-configured VillaConfig for dev
    http_clientHttpClient with test config
    mock_http_responseFactory 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:

    FixtureExpected validation resultReason
    goodSample.jsonPassOfficial valid sample
    wrongType.jsonFailorderId is integer, not string
    extraCol.jsonReference onlyDocuments 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:

    ModulePriority
    validation/pre_payment.pyCritical — guards payment
    auth/service.pyCritical — security
    payments/service.pyCritical — money
    orders/builder.pyHigh — data contract
    cli/main.pySmoke 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:

    JobCommand
    Lintruff check + ruff format --check
    Unitpytest tests/unit -m unit (Python 3.10–3.12)
    Integrationpytest tests/integration -m integration
    Buildfull suite + --cov-fail-under=80 + python -m build

    Related documents

  • CI_CD.md — CI/CD pipeline and quality gates
  • DESIGN.md — architecture and data flow
  • ../README.md — installation and quick start