Testing Guide

This guide covers the testing practices and guidelines for the NSGG Backend project.

Testing Structure

Our tests are organized by app and test type:

app_name/
├── tests/
│   ├── __init__.py
│   ├── factories.py
│   ├── test_models.py
│   ├── test_views.py
│   ├── test_serializers.py
│   └── test_utils.py

Test Categories

Unit Tests

Unit tests focus on testing individual components in isolation:

def test_product_price_validation():
    product = Product(name="Test Product", price=Decimal("-10.00"))
    with pytest.raises(ValidationError):
        product.full_clean()

Integration Tests

Integration tests verify the interaction between components:

def test_create_order_with_cart(authenticated_client, cart_with_items):
    response = authenticated_client.post("/api/v1/orders/", {
        "shipping_address_id": 1,
        "payment_method": "stripe"
    })
    assert response.status_code == 201
    assert "order_id" in response.data

API Tests

API tests verify the REST API endpoints:

class TestProductAPI:
    def test_list_products(self, client):
        response = client.get("/api/v1/products/")
        assert response.status_code == 200
        assert len(response.data["results"]) > 0

    def test_create_product(self, admin_client):
        data = {
            "name": "New Product",
            "price": "99.99",
            "category": 1
        }
        response = admin_client.post("/api/v1/products/", data)
        assert response.status_code == 201

Test Fixtures

We use pytest fixtures for test setup:

@pytest.fixture
def user():
    return UserFactory()

@pytest.fixture
def authenticated_client(client, user):
    client.force_authenticate(user=user)
    return client

@pytest.fixture
def product_with_variants():
    product = ProductFactory()
    ProductVariantFactory.create_batch(3, product=product)
    return product

Model Factories

We use factory_boy for creating test data:

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    email = factory.Sequence(lambda n: f"user{n}@example.com")
    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    password = factory.PostGenerationMethodCall("set_password", "password123")

class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Product

    name = factory.Sequence(lambda n: f"Product {n}")
    price = factory.Faker("pydecimal", left_digits=3, right_digits=2)
    category = factory.SubFactory(CategoryFactory)

Running Tests

Run All Tests

pytest

Run Tests with Coverage

pytest --cov=.

Run Specific Tests

# Run tests in a specific file
pytest path/to/test_file.py

# Run a specific test class
pytest path/to/test_file.py::TestClass

# Run a specific test method
pytest path/to/test_file.py::TestClass::test_method

Test Options

# Show print statements
pytest -s

# Show detailed test progress
pytest -v

# Stop on first failure
pytest -x

# Run failed tests first
pytest --failed-first

# Run tests matching pattern
pytest -k "test_pattern"

Test Configuration

Our pytest configuration in pytest.ini:

[pytest]
DJANGO_SETTINGS_MODULE = config.settings
python_files = tests.py test_*.py *_tests.py
addopts = --reuse-db
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests

Writing Good Tests

Test Naming

Follow this pattern: - test_<what>_<expected_behavior> - Example: test_create_product_with_invalid_price_raises_error

Test Structure

Follow the Arrange-Act-Assert pattern:

def test_create_order_calculates_total():
    # Arrange
    products = ProductFactory.create_batch(3)
    cart = CartFactory()
    for product in products:
        CartItemFactory(cart=cart, product=product)

    # Act
    order = Order.objects.create_from_cart(cart)

    # Assert
    expected_total = sum(item.product.price * item.quantity for item in cart.items.all())
    assert order.total == expected_total

Test Coverage

Aim for high test coverage but focus on critical paths:

  1. Model validation and methods
  2. API endpoints
  3. Business logic
  4. Edge cases and error conditions

Mocking

Use unittest.mock or pytest-mock for mocking:

def test_payment_processing(mocker):
    mock_stripe = mocker.patch("services.payment.stripe")
    mock_stripe.PaymentIntent.create.return_value = {"id": "pi_123", "client_secret": "secret"}

    payment_service = PaymentService()
    result = payment_service.create_payment(amount=1000)

    assert result["payment_intent_id"] == "pi_123"
    mock_stripe.PaymentIntent.create.assert_called_once_with(amount=1000, currency="usd")

Testing Async Code

Use pytest-asyncio for testing async code:

@pytest.mark.asyncio
async def test_async_notification():
    notification = await NotificationService.send_async(
        user_id=1,
        message="Test message"
    )
    assert notification.status == "sent"

Performance Testing

Use pytest-benchmark for performance tests:

def test_product_search_performance(benchmark):
    def search():
        Product.objects.filter(name__icontains="test").count()

    benchmark(search)

Continuous Integration

Our GitHub Actions workflow runs tests on every push:

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.12
      - name: Install dependencies
        run: |
          pip install -r requirements/testing.txt
      - name: Run tests
        run: |
          pytest --cov=.

Best Practices

  1. Keep tests focused and isolated
  2. Use meaningful test data
  3. Test edge cases and error conditions
  4. Keep tests maintainable and readable
  5. Use appropriate assertions
  6. Clean up test data
  7. Document complex test scenarios
  8. Use test doubles appropriately