Development
Testing
Running tests and writing test cases for ZineCore2
This guide covers running tests, writing new tests, and testing best practices for ZineCore2.
Running Tests
Run All Tests
cd backend
DJANGO_SETTINGS_MODULE=zinecore.settings.development \
../.venv/bin/python manage.py test
Expected output:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..................................
----------------------------------------------------------------------
Ran 34 tests in 2.145s
OK
Destroying test database for alias 'default'...
Run Specific App Tests
# Test catalog app only
python manage.py test catalog
# Test agents app only
python manage.py test agents
Run Specific Test Class
python manage.py test catalog.tests.TestZineAPI
Run Specific Test Method
python manage.py test catalog.tests.TestZineAPI.test_create_zine
Verbose Output
python manage.py test --verbosity=2
Shows individual test names as they run.
Keep Test Database
python manage.py test --keepdb
Faster for repeated test runs (reuses database).
Test Organization
Directory Structure
backend/
├── catalog/
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── test_models.py # Model tests
│ │ ├── test_serializers.py # Serializer tests
│ │ ├── test_views.py # API endpoint tests
│ │ └── test_filters.py # Filter tests
│ ├── models.py
│ ├── serializers.py
│ └── views.py
├── agents/
│ ├── tests/
│ │ └── ...
│ └── ...
Test File Naming
- test_*.py — Django auto-discovers these files
- test_models.py — Model-specific tests
- test_serializers.py — Serializer tests
- test_views.py — ViewSet/API tests
Writing Tests
Basic Test Structure
# catalog/tests/test_models.py
from django.test import TestCase
from catalog.models import Zine
from agents.models import Agent
class ZineModelTest(TestCase):
"""Test Zine model"""
def setUp(self):
"""Setup test data"""
self.agent = Agent.objects.create(
agent_id='agent_test',
kind='Person',
display_name='Test Agent',
public=True
)
def test_create_zine(self):
"""Test creating a zine"""
zine = Zine.objects.create(
zine_id='zine_test',
title='Test Zine',
creator=['agent_test'],
subject=['test'],
genre=['test-zine'],
date=['2024'],
language=['en'],
rights=['unknown']
)
self.assertEqual(zine.title, 'Test Zine')
self.assertEqual(zine.creator, ['agent_test'])
def test_zine_str(self):
"""Test zine string representation"""
zine = Zine.objects.create(
zine_id='zine_test',
title='Test Zine',
# ... required fields ...
)
self.assertEqual(str(zine), 'Test Zine')
API Endpoint Tests
# catalog/tests/test_views.py
from rest_framework.test import APITestCase
from rest_framework import status
from django.contrib.auth import get_user_model
from rest_framework.authtoken.models import Token
User = get_user_model()
class ZineAPITest(APITestCase):
"""Test Zine API endpoints"""
def setUp(self):
"""Setup authentication and test data"""
# Create user and token
self.user = User.objects.create_user(
username='testuser',
password='testpass'
)
self.token = Token.objects.create(user=self.user)
# Set authentication
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')
def test_list_zines(self):
"""Test GET /api/zines/"""
response = self.client.get('/api/zines/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('results', response.data)
def test_create_zine(self):
"""Test POST /api/zines/"""
data = {
'zine_id': 'zine_test',
'title': 'Test Zine',
'creator': ['agent_test'],
'subject': ['test'],
'genre': ['test-zine'],
'date': ['2024'],
'language': ['en'],
'rights': ['unknown']
}
response = self.client.post('/api/zines/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], 'Test Zine')
def test_create_zine_missing_required_field(self):
"""Test creating zine without required field fails"""
data = {
'zine_id': 'zine_test',
'title': 'Test Zine',
# Missing creator (required)
}
response = self.client.post('/api/zines/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('creator', response.data)
def test_create_zine_unauthenticated(self):
"""Test creating zine without authentication fails"""
# Remove authentication
self.client.credentials()
data = {
'zine_id': 'zine_test',
'title': 'Test Zine',
# ... fields ...
}
response = self.client.post('/api/zines/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Serializer Tests
# catalog/tests/test_serializers.py
from django.test import TestCase
from catalog.serializers import ZineWriteSerializer, ZineReadSerializer
from catalog.models import Zine
class ZineSerializerTest(TestCase):
"""Test Zine serializers"""
def test_write_serializer_valid(self):
"""Test write serializer with valid data"""
data = {
'zine_id': 'zine_test',
'title': 'Test Zine',
'creator': ['agent_test'],
'subject': ['test'],
'genre': ['test-zine'],
'date': ['2024'],
'language': ['en'],
'rights': ['unknown']
}
serializer = ZineWriteSerializer(data=data)
self.assertTrue(serializer.is_valid())
def test_write_serializer_missing_required_field(self):
"""Test write serializer rejects missing required field"""
data = {
'zine_id': 'zine_test',
'title': 'Test Zine',
# Missing creator
}
serializer = ZineWriteSerializer(data=data)
self.assertFalse(serializer.is_valid())
self.assertIn('creator', serializer.errors)
def test_read_serializer_resolves_creator(self):
"""Test read serializer resolves creator to full object"""
# Create agent first
# ... agent creation ...
# Create zine
zine = Zine.objects.create(
zine_id='zine_test',
creator=['agent_test'],
# ... fields ...
)
serializer = ZineReadSerializer(zine)
# Check creator is resolved
self.assertIsInstance(serializer.data['creator'], list)
self.assertEqual(serializer.data['creator'][0]['agent_id'], 'agent_test')
Test Patterns
Testing Authentication
def test_endpoint_requires_auth(self):
"""Test endpoint requires authentication"""
# Remove authentication
self.client.credentials()
response = self.client.post('/api/zines/', {})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
Testing Validation
def test_invalid_subject_code(self):
"""Test invalid subject code is rejected"""
data = {
'zine_id': 'zine_test',
'subject': ['invalid-subject-code'],
# ... fields ...
}
response = self.client.post('/api/zines/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn('subject', response.data)
Testing Filtering
def test_filter_by_subject(self):
"""Test filtering zines by subject"""
# Create zines with different subjects
# ...
response = self.client.get('/api/zines/?subject=feminism')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# All results should have subject 'feminism'
for zine in response.data['results']:
self.assertIn('feminism', zine['subject'])
Testing Search
def test_search_by_title(self):
"""Test searching zines by title"""
# Create zines
# ...
response = self.client.get('/api/zines/?search=mutate')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Results should include 'mutate' in title or abstract
Testing Pagination
def test_pagination(self):
"""Test pagination works"""
# Create 30 zines
for i in range(30):
Zine.objects.create(
zine_id=f'zine_{i:03d}',
# ... fields ...
)
response = self.client.get('/api/zines/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['results']), 25) # Default page size
self.assertIsNotNone(response.data['next']) # Has next page
Test Fixtures
Using Fixtures
# catalog/tests/test_views.py
class ZineAPITest(APITestCase):
fixtures = ['agents.json', 'subjects.json', 'genres.json']
def test_create_zine_with_fixtures(self):
"""Test creating zine using fixture data"""
# Agents/subjects/genres loaded from fixtures
# ...
Creating Fixtures
# Export current data as fixture
python manage.py dumpdata catalog.Zine --indent 2 > catalog/fixtures/zines.json
Code Coverage
Install Coverage
pip install coverage
Run Tests with Coverage
coverage run --source='.' manage.py test
coverage report
Output:
Name Stmts Miss Cover
-------------------------------------------
catalog/models.py 142 5 96%
catalog/serializers.py 89 2 98%
catalog/views.py 67 3 96%
-------------------------------------------
TOTAL 298 10 97%
HTML Coverage Report
coverage html
# Open htmlcov/index.html in browser
Shows line-by-line coverage.
Target Coverage
Aim for:
- Overall: 90%+
- Models: 95%+
- Serializers: 95%+
- Views: 85%+
Test Best Practices
DO:
✅ Test one thing per test
def test_create_zine(self):
"""Test creating a zine"""
# Only tests creation, not update/delete
def test_update_zine(self):
"""Test updating a zine"""
# Only tests update
✅ Use descriptive test names
# Good
def test_create_zine_with_empty_creator_returns_400(self):
pass
# Bad
def test_zine(self):
pass
✅ Test edge cases
def test_create_zine_with_max_length_title(self):
"""Test zine with max length title (500 chars)"""
# ...
def test_create_zine_with_empty_array_fields(self):
"""Test zine with empty optional array fields"""
# ...
✅ Clean up test data in tearDown
def tearDown(self):
"""Clean up after tests"""
Zine.objects.all().delete()
✅ Use factories for complex objects
# Using factory_boy
class ZineFactory(factory.django.DjangoModelFactory):
class Meta:
model = Zine
zine_id = factory.Sequence(lambda n: f'zine_{n:03d}')
title = factory.Faker('sentence')
# ...
# In tests
zine = ZineFactory.create()
DON'T:
❌ Test implementation details
Test behavior, not how it's implemented.
❌ Make tests dependent on each other
Each test should be independent.
❌ Skip assertions
Always assert expected results.
❌ Use production database
Always use test database.
Debugging Failed Tests
Run Single Test
python manage.py test catalog.tests.TestZineAPI.test_create_zine --verbosity=2
Add Print Statements
def test_create_zine(self):
response = self.client.post('/api/zines/', data)
print(f"Response status: {response.status_code}")
print(f"Response data: {response.data}")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Use pdb Debugger
def test_create_zine(self):
import pdb; pdb.set_trace()
response = self.client.post('/api/zines/', data)
# ...
Check Test Database
def test_create_zine(self):
response = self.client.post('/api/zines/', data)
# Check what was actually created
zine = Zine.objects.get(zine_id='zine_test')
print(f"Created zine: {zine}")
Continuous Integration
Tests run automatically on GitHub Actions for all pull requests.
CI configuration:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.12'
- name: Install dependencies
run: |
pip install uv
uv sync
- name: Run tests
run: |
cd backend
python manage.py test
Next Steps
- Code Style — Coding standards
- Contributing — How to contribute
- Troubleshooting — Common issues
Tests passing? Great! Continue to Code Style for coding standards.