Skip to content

Instantly share code, notes, and snippets.

@scragz
Last active July 1, 2025 17:09
Show Gist options
  • Save scragz/7ca91ce7cc42342ba43b14eec60b7d7b to your computer and use it in GitHub Desktop.
Save scragz/7ca91ce7cc42342ba43b14eec60b7d7b to your computer and use it in GitHub Desktop.
TDD

Core Philosophy

TEST-DRIVEN DEVELOPMENT IS NON-NEGOTIABLE. Every line of production code is written in direct response to a failing test—no exceptions. TDD is the keystone that supports every other principle in this document.

I practice Test-Driven Development with a strong emphasis on behavior-driven testing and functional programming principles. Work proceeds in very small, incremental changes that keep the system green at all times.


Quick Reference

Key Principles

Principle Python-specific Notes
Write tests first Use pytest or pytest-bdd to express scenarios before implementation.
Test behavior, not implementation Treat modules as black boxes; assert on externally observable outcomes.
No “dynamic” escape hatches Avoid typing.Any, unchecked casts, or # type: ignore pragmas.
Immutable data only Prefer @dataclass(frozen=True), NamedTuple, or typing.TypedDict + frozen wrappers.
Small, pure functions Minimize side-effects; lean on composition over shared state.
Static type checking on Run mypy --strict (or pyright --strict) in CI; no warnings allowed.
Re-use real domain types in tests Import the same dataclasses / pydantic models your app uses—never re-declare shapes in test code.

Preferred Tools

  • Language: Python 3.12+ (PEP 695 type-parameter syntax)
  • Testing: pytest ± pytest-bdd or behave
  • Property Testing: hypothesis
  • Mocking / Stubbing: pytest-mock, responses, MSW-py (Mock Service Worker analogue for HTTPX / aiohttp)
  • Lint / Static Analysis: ruff, mypy --strict, pyright
  • State Management: Immutable data structures (attrs, frozen dataclasses, immutables.Map) and pure functions

Testing Principles

Behavior-Driven Testing

  • Scrap the term “unit test.” A test’s granularity is less important than its focus on externally visible behavior.
  • Interact strictly through the module’s public API—internal helpers are invisible.
  • No 1:1 test-to-file mapping. Organize tests around user stories, features, or public boundaries.
  • Tests that crack open internals are a maintenance burden—avoid them.
  • Coverage target: 100 % line + branch coverage, but every assertion must track to business behavior, not code paths.
  • A test suite is executable documentation of business expectations.

Testing Tools

Purpose Library
Test runner / assertions pytest
BDD syntax (optional) pytest-bdd or behave
Property-based checks hypothesis
HTTP / API mocking responses, pytest-httpx, or MSW-py
Static typing in tests Same strict mypy/pyright settings as production code

Test Layout & Naming

project_root/
  src/
    payments/
      processor.py              # public API
      _validator.py             # implementation detail (prefixed underscore)
  tests/
    test_payments.py            # covers validation *through* processor
    conftest.py                 # shared fixtures & factories
  • Tests live under tests/, mirroring features rather than files.
  • Private helpers (e.g., _validator.py) never appear in imports inside tests/.

Test Data Pattern

Use dataclass factories with optional overrides—type-safe, immutable, and composable.

# src/payments/models.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal

Currency = Literal["USD", "EUR", "GBP"]

@dataclass(frozen=True)
class Address:
    street: str
    city: str
    house_number: str

@dataclass(frozen=True)
class PostPaymentRequest:
    card_account_id: str
    amount: int  # cents
    currency: Currency
    source: Literal["Web", "Mobile"]
    last_name: str
    date_of_birth: str  # ISO8601 YYYY-MM-DD
    cvv: str
    token: str
    address: Address
    brand: Literal["Visa", "Mastercard", "Amex"]
# tests/factories.py
from datetime import date
from payments.models import Address, PostPaymentRequest

def make_address(**overrides) -> Address:
    defaults = dict(
        street="Test Address Line 1",
        city="Testville",
        house_number="123",
    )
    return Address(**{**defaults, **overrides})

def make_payment_request(**overrides) -> PostPaymentRequest:
    defaults = dict(
        card_account_id="1234_5678_9012_3456",
        amount=10_00,
        currency="USD",
        source="Web",
        last_name="Doe",
        date_of_birth=str(date(1980, 1, 1)),
        cvv="123",
        token="tok_test",
        address=make_address(),
        brand="Visa",
    )
    return PostPaymentRequest(**{**defaults, **overrides})

Factory Guidelines

  1. Return complete, valid objects with sensible defaults.
  2. Accept arbitrary keyword overrides—type-checked by mypy.
  3. Extract nested factories (make_address) as domains grow.
  4. Compose factories rather than duplicating literals.
  5. For extremely complex graphs, graduate to the Test Data Builder pattern (chained mutator methods returning new frozen instances).

Continuous Integration Checklist

  1. ruff check .
  2. mypy --strict src tests
  3. pytest --cov=src --cov-fail-under=100
  4. python -m pip check (dependency health)
  5. pre-commit run --all-files (formatters, linters)

All stages must pass before merging to main.


Why so strict?

  • Immutable data eliminates an entire class of state-related bugs.
  • Strict typing catches integration errors before runtime.
  • 100 % behavior coverage ensures refactors are fearless.
  • Small, pure functions maximize composability and cognitive clarity.

“If it isn’t proven by a failing test first, it doesn’t exist.” — You, every day

Use this guide as the non-negotiable contract for any Python project you touch.

Core Philosophy

TEST-DRIVEN DEVELOPMENT IS NON-NEGOTIABLE. Every single line of production code must be written in response to a failing test. No exceptions. This is not a suggestion or a preference - it is the fundamental practice that enables all other principles in this document.

I follow Test-Driven Development (TDD) with a strong emphasis on behavior-driven testing and functional programming principles. All work should be done in small, incremental changes that maintain a working state throughout development.

Quick Reference

Key Principles:

  • Write tests first (TDD)
  • Test behavior, not implementation
  • No any types or type assertions
  • Immutable data only
  • Small, pure functions
  • TypeScript strict mode always
  • Use real schemas/types in tests, never redefine them

Preferred Tools:

  • Language: TypeScript (strict mode)
  • Testing: Jest/Vitest + React Testing Library
  • State Management: Prefer immutable patterns

Testing Principles

Behavior-Driven Testing

  • No "unit tests" - this term is not helpful. Tests should verify expected behavior, treating implementation as a black box
  • Test through the public API exclusively - internals should be invisible to tests
  • No 1:1 mapping between test files and implementation files
  • Tests that examine internal implementation details are wasteful and should be avoided
  • Coverage targets: 100% coverage should be expected at all times, but these tests must ALWAYS be based on business behaviour, not implementation details
  • Tests must document expected business behaviour

Testing Tools

  • Jest or Vitest for testing frameworks
  • React Testing Library for React components
  • MSW (Mock Service Worker) for API mocking when needed
  • All test code must follow the same TypeScript strict mode rules as production code

Test Organization

src/
  features/
    payment/
      payment-processor.ts
      payment-validator.ts
      payment-processor.test.ts // The validator is an implementation detail. Validation is fully covered, but by testing the expected business behaviour, treating the validation code itself as an implementation detail

Test Data Pattern

Use factory functions with optional overrides for test data:

const getMockPaymentPostPaymentRequest = (
  overrides?: Partial<PostPaymentsRequestV3>
): PostPaymentsRequestV3 => {
  return {
    CardAccountId: "1234567890123456",
    Amount: 100,
    Source: "Web",
    AccountStatus: "Normal",
    LastName: "Doe",
    DateOfBirth: "1980-01-01",
    PayingCardDetails: {
      Cvv: "123",
      Token: "token",
    },
    AddressDetails: getMockAddressDetails(),
    Brand: "Visa",
    ...overrides,
  };
};

const getMockAddressDetails = (
  overrides?: Partial<AddressDetails>
): AddressDetails => {
  return {
    HouseNumber: "123",
    HouseName: "Test House",
    AddressLine1: "Test Address Line 1",
    AddressLine2: "Test Address Line 2",
    City: "Test City",
    ...overrides,
  };
};

Key principles:

  • Always return complete objects with sensible defaults
  • Accept optional Partial<T> overrides
  • Build incrementally - extract nested object factories as needed
  • Compose factories for complex objects
  • Consider using a test data builder pattern for very complex objects
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment