Rohan Yeole - Homepage Rohan Yeole

How to Add Tests to Code You Didn't Write (Including AI-Generated Code)

May 7, 20261 min read

The hardest part of adding tests to existing code — whether AI-generated or legacy — isn't technical. It's knowing where to start without getting overwhelmed.

The wrong approach is starting with unit tests for individual functions. The right approach is starting with integration tests that cover the paths that would hurt most if they broke.

The Starting Point: Critical Path Identification

Before writing a single test, list the things that must not break:

  1. Authentication — users can sign up, log in, log out, reset passwords
  2. The core action — whatever the app is for: placing an order, submitting a form, creating a record
  3. Payment flows — if there's billing, every payment path
  4. Data integrity — mutations that write to the database

These are your first tests. Everything else can wait.

Setup: pytest-django

Django's built-in test runner works, but pytest-django is better for existing codebases — it's faster, has better fixtures, and produces readable output.

pip install pytest pytest-django factory-boy
# pytest.ini (or pyproject.toml [tool.pytest.ini_options])
[pytest]
DJANGO_SETTINGS_MODULE = myapp.settings
python_files = tests.py test_*.py *_tests.py
# conftest.py — shared fixtures
import pytest
from django.test import Client

@pytest.fixture
def client():
    return Client()

@pytest.fixture
def auth_client(db, django_user_model):
    user = django_user_model.objects.create_user(
        username="testuser",
        email="[email protected]",
        password="testpass123"
    )
    client = Client()
    client.login(username="testuser", password="testpass123")
    return client, user

Your First Test: Authentication Works

# tests/test_auth.py
import pytest
from django.urls import reverse

@pytest.mark.django_db
def test_user_can_register(client):
    response = client.post(reverse("register"), {
        "username": "newuser",
        "email": "[email protected]",
        "password1": "complex_password_123",
        "password2": "complex_password_123",
    })
    assert response.status_code in [200, 302]
    from django.contrib.auth import get_user_model
    User = get_user_model()
    assert User.objects.filter(username="newuser").exists()

@pytest.mark.django_db
def test_user_can_login(client, django_user_model):
    django_user_model.objects.create_user(username="existing", password="pass123")
    response = client.post(reverse("login"), {
        "username": "existing",
        "password": "pass123"
    }, follow=True)
    assert response.status_code == 200

@pytest.mark.django_db
def test_login_required_redirects_to_login(client):
    response = client.get(reverse("dashboard"))
    assert response.status_code == 302
    assert "/login" in response["Location"]

Run them: pytest tests/test_auth.py -v

Factory Boy: Creating Test Data Without Fixtures

Fixtures (JSON/YAML) break when your models change. Factory Boy creates test data programmatically:

# tests/factories.py
import factory
from factory.django import DjangoModelFactory
from django.contrib.auth import get_user_model
from myapp.models import Order, Product

class UserFactory(DjangoModelFactory):
    class Meta:
        model = get_user_model()

    username = factory.Sequence(lambda n: f"user{n}")
    email = factory.LazyAttribute(lambda o: f"{o.username}@example.com")
    password = factory.PostGenerationMethodCall("set_password", "password123")

class ProductFactory(DjangoModelFactory):
    class Meta:
        model = Product

    name = factory.Sequence(lambda n: f"Product {n}")
    price = factory.Faker("pydecimal", left_digits=3, right_digits=2, positive=True)
    stock = 10

class OrderFactory(DjangoModelFactory):
    class Meta:
        model = Order

    user = factory.SubFactory(UserFactory)
    product = factory.SubFactory(ProductFactory)
    quantity = 1
# tests/test_orders.py
import pytest
from .factories import UserFactory, OrderFactory, ProductFactory

@pytest.mark.django_db
def test_order_reduces_stock():
    product = ProductFactory(stock=5)
    order = OrderFactory(product=product, quantity=2)
    order.process()  # whatever your processing logic is
    product.refresh_from_db()
    assert product.stock == 3

@pytest.mark.django_db
def test_order_belongs_to_user(auth_client):
    client, user = auth_client
    other_user_order = OrderFactory()  # belongs to a different user
    response = client.get(f"/orders/{other_user_order.id}/")
    assert response.status_code in [403, 404]  # not 200 — that's the bug

The last test is the most important one to write for AI-generated code — it catches IDOR (accessing another user's data), which AI almost always generates incorrectly.

Testing AI-Generated Views: What to Focus On

AI-generated views have predictable failure modes. Test these specifically:

Permission gates

@pytest.mark.django_db
def test_unauthenticated_cannot_access_dashboard(client):
    response = client.get("/dashboard/")
    assert response.status_code == 302  # redirect to login

@pytest.mark.django_db
def test_user_cannot_access_other_users_data(auth_client):
    client, user = auth_client
    other_order = OrderFactory()  # created by a different user via SubFactory
    response = client.get(f"/orders/{other_order.id}/")
    assert response.status_code in [403, 404]

Form validation

@pytest.mark.django_db
def test_form_rejects_empty_required_fields(auth_client):
    client, user = auth_client
    response = client.post("/submit/", {"title": ""})  # required field empty
    assert response.status_code == 200  # form re-renders, not redirects
    assert "required" in response.content.decode().lower()

@pytest.mark.django_db
def test_form_rejects_negative_price(auth_client):
    client, user = auth_client
    response = client.post("/products/create/", {
        "name": "Widget",
        "price": "-10.00",
        "stock": 5,
    })
    assert response.status_code == 200  # not 302, not processing it

API endpoints return correct status codes

@pytest.mark.django_db
def test_api_returns_404_for_nonexistent(auth_client):
    client, user = auth_client
    response = client.get("/api/orders/99999/")
    assert response.status_code == 404

@pytest.mark.django_db
def test_api_post_requires_csrf_token(client):
    response = client.post(
        "/api/submit/",
        data='{"title": "test"}',
        content_type="application/json",
        enforce_csrf_checks=True,
    )
    assert response.status_code == 403  # CSRF check should block this

Testing Celery Tasks

AI-generated async tasks often have bugs because they're never actually tested:

# tests/test_tasks.py
import pytest
from myapp.tasks import send_welcome_email

@pytest.mark.django_db
def test_send_welcome_email_task(mailoutbox, django_user_model):
    user = django_user_model.objects.create_user(
        username="taskuser",
        email="[email protected]",
        password="pass123"
    )
    send_welcome_email(user.id)  # call synchronously in tests
    assert len(mailoutbox) == 1
    assert mailoutbox[0].to == ["[email protected]"]

Use CELERY_TASK_ALWAYS_EAGER = True in test settings so tasks run synchronously:

# settings.py
if TESTING := os.environ.get("TESTING"):
    CELERY_TASK_ALWAYS_EAGER = True

Running Tests in CI

Add a GitHub Actions workflow so tests run on every push:

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        ports: ["5432:5432"]
        options: --health-cmd pg_isready --health-interval 10s

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest --tb=short
        env:
          DATABASE_URL: postgres://testuser:testpass@localhost:5432/testdb
          DJANGO_SECRET_KEY: test-secret-key-not-real
          TESTING: "1"

The 20% That Covers 80% of the Risk

If your app has zero tests and you need to ship:

  1. Test that every auth-required view redirects unauthenticated users
  2. Test that users can only access their own objects (the IDOR check)
  3. Test the core happy path end-to-end
  4. Test that the payment/billing flow completes correctly

Those four test categories will catch the bugs that cause incidents. Write them first, then expand coverage from there.

If you need tests added to your AI-built Django codebase before it goes to production, hire me for a code review and test suite setup. I'll write the critical path tests and set up CI so they run automatically.

Chat with me on WhatsApp