API Versioning
API versioning is how teams evolve their APIs without breaking existing clients. As a QA engineer, you must test that new versions work correctly AND that old versions continue to function. Breaking backward compatibility is one of the most impactful bugs you can catch.
Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/users, /v2/users |
Clear, easy to route | URL pollution, hard to remove old versions |
| Header | Accept: application/vnd.api+json; version=2 |
Clean URLs | Harder to test manually, easy to miss |
| Query param | /users?version=2 |
Simple | URL pollution, caching complications |
| Content negotiation | Accept: application/vnd.company.v2+json |
RESTful | Complex to implement and test |
URL Path Versioning (Most Common)
def test_v1_users_endpoint(api):
r = api.get("/v1/users")
assert r.status_code == 200
user = r.json()["items"][0]
assert "name" in user # v1 returns "name"
def test_v2_users_endpoint(api):
r = api.get("/v2/users")
assert r.status_code == 200
user = r.json()["items"][0]
assert "first_name" in user # v2 splits into first/last
assert "last_name" in user
Header-Based Versioning
def test_header_versioning(api, base_url):
# Default version (no header)
r = api.get("/users")
assert r.status_code == 200
default_fields = set(r.json()["items"][0].keys())
# Explicit v2
r = api.get("/users", headers={"API-Version": "2"})
assert r.status_code == 200
v2_fields = set(r.json()["items"][0].keys())
# v2 should have new fields
assert "first_name" in v2_fields
assert "last_name" in v2_fields
def test_missing_version_header_uses_default(api):
"""When no version header is sent, the API should use the default version."""
r = api.get("/users")
assert r.status_code == 200
# Verify it returns the default version's response format
Backward Compatibility Testing
The most important aspect of API versioning testing: old clients must not break when new versions launch.
def test_v1_still_works_after_v2_launch(api):
"""v1 endpoints must continue to function correctly."""
# Create a user via v1
r = api.post("/v1/users", json={"name": "Alice", "email": "alice@test.com"})
assert r.status_code == 201
# Read via v1
user_id = r.json()["id"]
r = api.get(f"/v1/users/{user_id}")
assert r.status_code == 200
assert r.json()["name"] == "Alice"
# Delete via v1
r = api.delete(f"/v1/users/{user_id}")
assert r.status_code == 204
def test_v1_data_accessible_via_v2(api):
"""Data created in v1 should be accessible in v2 format."""
# Create in v1 format
r = api.post("/v1/users", json={"name": "Bob Smith", "email": "bob@test.com"})
user_id = r.json()["id"]
# Read in v2 format
r = api.get(f"/v2/users/{user_id}")
assert r.status_code == 200
# v2 should correctly split the name
assert r.json()["first_name"] == "Bob"
assert r.json()["last_name"] == "Smith"
def test_v2_data_accessible_via_v1(api):
"""Data created in v2 should be accessible in v1 format."""
r = api.post("/v2/users", json={
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@test.com"
})
user_id = r.json()["id"]
# Read in v1 format
r = api.get(f"/v1/users/{user_id}")
assert r.status_code == 200
assert r.json()["name"] == "Jane Doe" # v1 should combine first/last
Deprecation Testing
def test_deprecated_endpoint_returns_warning(api):
"""Deprecated endpoints should include a deprecation warning header."""
r = api.get("/v1/legacy-endpoint")
assert r.status_code == 200 # Still works
# Should include deprecation warning
assert "Deprecation" in r.headers or "Sunset" in r.headers or \
"X-Deprecated" in r.headers
def test_deprecated_endpoint_still_functional(api):
"""Deprecated endpoints must still work until officially removed."""
r = api.get("/v1/legacy-endpoint")
assert r.status_code == 200
assert len(r.json()["items"]) > 0 # Still returns data
Common Versioning Bugs
| Bug | Test Strategy |
|---|---|
| v1 breaks after v2 deployment | Run full v1 test suite after v2 is deployed |
| Data created in v1 not visible in v2 | Cross-version CRUD tests |
| Default version changes unexpectedly | Test requests without explicit version |
| Error responses differ between versions | Verify error format is consistent |
| Deprecated endpoint stops working before sunset date | Monitor deprecated endpoints |
| v2 returns v1 format fields | Verify v2 response schema strictly |
Version Discovery
def test_api_exposes_available_versions(api, base_url):
"""API should document available versions."""
r = api.get("/")
if r.status_code == 200:
data = r.json()
# Some APIs list available versions at the root
if "versions" in data:
assert "v1" in data["versions"]
assert "v2" in data["versions"]
def test_unsupported_version_returns_error(api, base_url):
"""Requesting a non-existent version should return a clear error."""
r = api.get("/v99/users")
assert r.status_code in (400, 404)
Practical Exercise
- Write backward compatibility tests: create resources in v1, verify they are accessible in v2
- Write cross-version data tests: create in v2, read in v1
- Test the default version behavior (no version specified)
- Test deprecated endpoints: verify they still work and include deprecation headers
- Write a test matrix that covers all CRUD operations across all API versions
Key Takeaways
- Always test backward compatibility: v1 must not break when v2 launches
- Test cross-version data access: data created in one version should be readable in another
- Test default version behavior: what happens when no version is specified?
- Monitor deprecated endpoints: they must work until the announced sunset date
- URL path versioning is most common; header versioning is cleanest but harder to test manually
- Version compatibility testing prevents one of the most impactful types of API bugs
Interview Talking Point: "I build API test suites in pytest with the requests library, organized around authentication, CRUD operations, error handling, and edge cases like rate limiting and pagination. I test both success paths and failure modes — expired tokens, malformed payloads, information leakage in error responses. I parameterize environments so the same suite runs against dev, staging, and production with a single variable change."