AI-Generated API Test Examples
Complete Generated Test Output
This file shows what a well-prompted AI produces from an OpenAPI schema, annotated with commentary on what makes each test effective.
The Generated Test File
import pytest
import httpx
from uuid import uuid4
BASE_URL = "https://api.staging.example.com"
# ============================================================
# Fixtures
# ============================================================
@pytest.fixture
def auth_token():
"""Get a valid user-level auth token."""
return get_test_token(role="user")
@pytest.fixture
def admin_token():
"""Get a valid admin-level auth token."""
return get_test_token(role="admin")
@pytest.fixture
def seed_product(admin_token):
"""Create a product for testing, clean up afterward."""
response = httpx.post(
f"{BASE_URL}/api/v2/products",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": "Test Widget", "price": 29.99, "category": "electronics"}
)
product = response.json()
yield product
# Cleanup
httpx.delete(
f"{BASE_URL}/api/v2/products/{product['id']}",
headers={"Authorization": f"Bearer {admin_token}"}
)
# ============================================================
# GET /api/v2/products/{id}
# ============================================================
class TestGetProduct:
"""GET /api/v2/products/{id}"""
def test_get_existing_product(self, auth_token, seed_product):
"""Happy path: retrieve a known product."""
response = httpx.get(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {auth_token}"}
)
assert response.status_code == 200
body = response.json()
assert body["id"] == seed_product["id"]
assert body["name"] == seed_product["name"]
assert body["category"] in ["electronics", "clothing", "food", "other"]
assert isinstance(body["price"], (int, float))
assert body["price"] >= 0
def test_get_nonexistent_product(self, auth_token):
"""404 when product ID does not exist."""
fake_id = str(uuid4())
response = httpx.get(
f"{BASE_URL}/api/v2/products/{fake_id}",
headers={"Authorization": f"Bearer {auth_token}"}
)
assert response.status_code == 404
def test_get_product_invalid_uuid(self, auth_token):
"""400 or 422 when ID is not a valid UUID."""
response = httpx.get(
f"{BASE_URL}/api/v2/products/not-a-uuid",
headers={"Authorization": f"Bearer {auth_token}"}
)
assert response.status_code in [400, 422]
def test_get_product_no_auth(self):
"""401 when no authorization header is provided."""
response = httpx.get(f"{BASE_URL}/api/v2/products/{uuid4()}")
assert response.status_code == 401
# ============================================================
# PUT /api/v2/products/{id}
# ============================================================
class TestUpdateProduct:
"""PUT /api/v2/products/{id}"""
def test_update_product_as_admin(self, admin_token, seed_product):
"""Happy path: admin updates a product."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": "Updated Widget", "price": 29.99, "category": "electronics"}
)
assert response.status_code == 200
assert response.json()["name"] == "Updated Widget"
def test_update_product_as_non_admin(self, auth_token, seed_product):
"""403 when non-admin tries to update."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {auth_token}"},
json={"name": "Hacked", "price": 0}
)
assert response.status_code == 403
def test_update_missing_required_name(self, admin_token, seed_product):
"""400 when required field 'name' is missing."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"price": 10.00} # name is missing
)
assert response.status_code == 400
def test_update_missing_required_price(self, admin_token, seed_product):
"""400 when required field 'price' is missing."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": "Widget"} # price is missing
)
assert response.status_code == 400
def test_update_negative_price(self, admin_token, seed_product):
"""400 when price is below minimum (0)."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": "Widget", "price": -5.00}
)
assert response.status_code == 400
@pytest.mark.parametrize("name", [
"", # Below minLength (0 chars)
"A" * 201, # Above maxLength (201 chars)
])
def test_update_name_boundary(self, admin_token, seed_product, name):
"""400 when name violates length constraints."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": name, "price": 10.00}
)
assert response.status_code == 400
@pytest.mark.parametrize("name", [
"A", # At minLength (1 char)
"A" * 200, # At maxLength (200 chars)
"Normal Product", # Nominal value
])
def test_update_name_valid_boundaries(self, admin_token, seed_product, name):
"""200 when name is within valid length range."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": name, "price": 10.00}
)
assert response.status_code == 200
def test_update_invalid_category_enum(self, admin_token, seed_product):
"""400 when category is not in the allowed enum."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": "Widget", "price": 10.00, "category": "weapons"}
)
assert response.status_code == 400
@pytest.mark.parametrize("category", [
"electronics", "clothing", "food", "other"
])
def test_update_valid_category_values(self, admin_token, seed_product, category):
"""200 for each valid enum value."""
response = httpx.put(
f"{BASE_URL}/api/v2/products/{seed_product['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"name": "Widget", "price": 10.00, "category": category}
)
assert response.status_code == 200
assert response.json()["category"] == category
Annotation: What Makes These Tests Good
1. Fixture-Based Setup and Teardown
The seed_product fixture creates test data and cleans up afterward. This ensures test independence -- no test depends on state from a previous test.
2. Multi-Layer Assertions
Good tests check more than just the status code:
# Not just: assert response.status_code == 200
# But also:
assert body["id"] == seed_product["id"] # Correct resource returned
assert body["category"] in [...] # Valid enum
assert body["price"] >= 0 # Business constraint
3. Parametrized Boundary Tests
Instead of writing separate test functions for each boundary value, parametrize combines them into a compact, maintainable structure.
4. Both Valid and Invalid Boundaries
Note that there are TWO parametrized tests for name length:
test_update_name_boundarytests INVALID values (expect 400)test_update_name_valid_boundariestests VALID values (expect 200)
This ensures both sides of the boundary are covered.
5. Every Documented Status Code Has a Test
The schema documents 200, 400, 401, and 403. Each has at least one test that triggers it.
Common Improvements Needed After Generation
| Issue | How to Fix |
|---|---|
| BASE_URL hardcoded | Move to environment variable or pytest fixture |
get_test_token undefined |
Create a conftest.py fixture or helper module |
| Missing timeout on HTTP calls | Add timeout=10.0 to all httpx calls |
| No error message assertions | Add assert "name" in response.json()["detail"] for 400 responses |
| Missing Content-Type header | Add "Content-Type": "application/json" to headers |
| No test for PATCH vs PUT semantics | If both exist, test that PUT replaces and PATCH merges |
Key Takeaway
AI-generated API tests from OpenAPI schemas are remarkably good when the prompt includes the full schema with $ref resolution, specifies the framework and auth mechanism, and requests explicit test categories (happy path, errors, boundaries, auth). The main review task is verifying that fixtures and helpers exist in your codebase and that error message assertions are specific enough.