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¶
Run Tests with Coverage¶
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:
- Model validation and methods
- API endpoints
- Business logic
- 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¶
- Keep tests focused and isolated
- Use meaningful test data
- Test edge cases and error conditions
- Keep tests maintainable and readable
- Use appropriate assertions
- Clean up test data
- Document complex test scenarios
- Use test doubles appropriately