QA Engineer Skills 2026QA-2026API Versioning

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

  1. Write backward compatibility tests: create resources in v1, verify they are accessible in v2
  2. Write cross-version data tests: create in v2, read in v1
  3. Test the default version behavior (no version specified)
  4. Test deprecated endpoints: verify they still work and include deprecation headers
  5. 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."