Claude
Skills
Sign in
Back

pytest-domain-model-testing

Included with Lifetime
$97 forever

How to test domain models effectively: value object testing (immutability, validation), entity testing (identity, business logic), domain exception testing, aggregate testing, high coverage patterns (95%+), and testing invariants and constraints. Use when: Testing domain layer code, validating value objects, testing entities with business logic, ensuring domain invariants, or achieving 95%+ coverage on domain models.

Code Review

What this skill does


# Pytest Domain Model Testing

## Purpose

The domain layer contains business logic and should have near-perfect coverage (95-100%). Domain models have zero external dependencies, making them easy to test thoroughly. This skill focuses on testing pure domain logic effectively.


## When to Use This Skill

Use when testing domain models with "test value objects", "test entities", "test domain logic", or "achieve 95% domain coverage".

Do NOT use for application layer (use `pytest-application-layer-testing`), adapters (use `pytest-adapter-integration-testing`), or mocking (domain tests should use real objects).
## Quick Start

Test domain models directly without mocks:

```python
from app.extraction.domain.value_objects import ProductTitle
import pytest

def test_product_title_validation() -> None:
    """Test value object validation."""
    # ✅ Valid
    title = ProductTitle("Awesome Laptop")
    assert title.value == "Awesome Laptop"

    # ❌ Invalid: too short
    with pytest.raises(ValueError, match="must be 1-500"):
        ProductTitle("")

    # ❌ Invalid: too long
    with pytest.raises(ValueError, match="must be 1-500"):
        ProductTitle("x" * 501)
```

## Instructions

### Step 1: Test Value Objects (Immutability & Validation)

```python
from __future__ import annotations

import pytest
from app.extraction.domain.value_objects import ProductTitle, Money, OrderId

class TestProductTitle:
    """Test ProductTitle value object."""

    def test_valid_creation(self) -> None:
        """Test creating valid product title."""
        title = ProductTitle("Laptop")
        assert title.value == "Laptop"

    def test_empty_title_raises_error(self) -> None:
        """Test that empty title is invalid."""
        with pytest.raises(ValueError, match="must be 1-500 characters"):
            ProductTitle("")

    def test_too_long_title_raises_error(self) -> None:
        """Test that title over 500 chars is invalid."""
        with pytest.raises(ValueError, match="must be 1-500 characters"):
            ProductTitle("x" * 501)

    def test_boundary_exactly_500_chars(self) -> None:
        """Test boundary: exactly 500 characters is valid."""
        title = ProductTitle("x" * 500)
        assert len(title.value) == 500

    def test_immutability(self) -> None:
        """Test value object is immutable (frozen dataclass)."""
        title = ProductTitle("Laptop")

        with pytest.raises(AttributeError):
            title.value = "Mouse"  # Should fail

    def test_unicode_characters(self) -> None:
        """Test title with unicode works."""
        title = ProductTitle("Café ☕ Deluxe")
        assert title.value == "Café ☕ Deluxe"

    def test_whitespace_handling(self) -> None:
        """Test title with whitespace."""
        title = ProductTitle("  Laptop  ")
        assert title.value == "  Laptop  "  # Preserves whitespace

    def test_equality(self) -> None:
        """Test two titles with same value are equal."""
        title1 = ProductTitle("Laptop")
        title2 = ProductTitle("Laptop")
        assert title1 == title2

    def test_inequality(self) -> None:
        """Test two titles with different values are not equal."""
        title1 = ProductTitle("Laptop")
        title2 = ProductTitle("Mouse")
        assert title1 != title2

    def test_hashable(self) -> None:
        """Test value object can be hashed (for sets/dicts)."""
        title1 = ProductTitle("Laptop")
        title2 = ProductTitle("Laptop")
        title3 = ProductTitle("Mouse")

        titles_set = {title1, title2, title3}
        assert len(titles_set) == 2  # title1 and title2 are same

    def test_string_representation(self) -> None:
        """Test __str__ returns value."""
        title = ProductTitle("Laptop")
        assert str(title) == "Laptop"
```

### Step 2: Test Entities (Identity & Business Logic)

```python
from __future__ import annotations

from datetime import datetime
import pytest

from app.extraction.domain.entities import Order, LineItem
from app.extraction.domain.value_objects import OrderId, ProductId, ProductTitle, Money

class TestOrderEntity:
    """Test Order aggregate."""

    def test_valid_order_creation(self) -> None:
        """Test creating valid order."""
        order = Order(
            order_id=OrderId("123"),
            created_at=datetime.now(),
            customer_name="John",
            line_items=[
                LineItem(
                    product_id=ProductId("prod_1"),
                    product_title=ProductTitle("Laptop"),
                    quantity=1,
                    price=Money.from_float(999.99),
                )
            ],
            total_price=Money.from_float(999.99),
        )

        assert order.order_id.value == "123"
        assert order.customer_name == "John"

    def test_empty_line_items_invalid(self) -> None:
        """Test order must have at least one line item."""
        with pytest.raises(ValueError, match="must have at least one line item"):
            Order(
                order_id=OrderId("123"),
                created_at=datetime.now(),
                customer_name="John",
                line_items=[],  # Invalid!
                total_price=Money.from_float(0.0),
            )

    def test_total_mismatch_invalid(self) -> None:
        """Test order total must match sum of line items."""
        with pytest.raises(ValueError, match="total mismatch"):
            Order(
                order_id=OrderId("123"),
                created_at=datetime.now(),
                customer_name="John",
                line_items=[
                    LineItem(
                        product_id=ProductId("prod_1"),
                        product_title=ProductTitle("Laptop"),
                        quantity=1,
                        price=Money.from_float(999.99),
                    )
                ],
                total_price=Money.from_float(500.00),  # Wrong total!
            )

    def test_negative_quantity_invalid(self) -> None:
        """Test line item quantity must be positive."""
        with pytest.raises(ValueError, match="quantity must be positive"):
            LineItem(
                product_id=ProductId("prod_1"),
                product_title=ProductTitle("Laptop"),
                quantity=-5,  # Invalid!
                price=Money.from_float(999.99),
            )

    def test_get_product_titles_behavior(self) -> None:
        """Test domain behavior: extract product titles."""
        order = Order(
            order_id=OrderId("123"),
            created_at=datetime.now(),
            customer_name="John",
            line_items=[
                LineItem(
                    product_id=ProductId("prod_1"),
                    product_title=ProductTitle("Laptop"),
                    quantity=1,
                    price=Money.from_float(999.99),
                ),
                LineItem(
                    product_id=ProductId("prod_2"),
                    product_title=ProductTitle("Mouse"),
                    quantity=2,
                    price=Money.from_float(29.99),
                ),
            ],
            total_price=Money.from_float(1059.97),
        )

        titles = order.get_product_titles()
        assert titles == ["Laptop", "Mouse"]

    def test_order_identity_by_id(self) -> None:
        """Test orders with same ID but different data are considered same."""
        order1 = Order(
            order_id=OrderId("same_id"),
            created_at=datetime.now(),
            customer_name="John",
            line_items=[LineItem(...)],
            total_price=Money.from_float(100.0),
        )

        order2 = Order(
            order_id=OrderId("same_id"),
            created_at=datetime.now(),
            customer_name="Jane",  # Different name
            line_items=[LineItem(...)],
            total_price=Money.from_float(100.0),
        )

        # Entities with same ID are considered equal
        assert

Related in Code Review