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'])
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

Tests passing? Great! Continue to Code Style for coding standards.
Copyright ©2026 ZineCore2 Contributors,