API Versioning Testing

Test API versioning strategies including v1 vs v2 endpoints and migration

LoadForge can record your browser, graphically build tests, scan your site with a wizard and more. Sign up now to run your first test.

Sign up now


This guide demonstrates how to test API versioning with LoadForge, covering version compatibility, migration scenarios, and deprecation handling.

Use Cases

  • Testing backward compatibility between API versions
  • Validating version-specific functionality and responses
  • Testing API migration and deprecation workflows
  • Load testing multiple API versions simultaneously
  • Validating version negotiation and routing

Basic API Versioning Testing

from locust import HttpUser, task, between
import json
import time
import random
import uuid

class APIVersioningUser(HttpUser):
    wait_time = between(1, 3)
    
    def on_start(self):
        """Initialize API versioning testing"""
        self.api_versions = ["v1", "v2", "v3"]
        self.auth_token = None
        self.test_data = {}
        
        # Authenticate (version-agnostic)
        self._authenticate()
        
    def _authenticate(self):
        """Authenticate with API (usually version-agnostic)"""
        auth_data = {
            "username": f"testuser_{random.randint(1000, 9999)}",
            "password": "test_password_123"
        }
        
        response = self.client.post('/auth/login',
                                  json=auth_data,
                                  headers={'Content-Type': 'application/json'},
                                  name="auth_login")
        
        if response.status_code == 200:
            try:
                auth_result = response.json()
                self.auth_token = auth_result.get('access_token')
            except json.JSONDecodeError:
                pass

    def _get_headers(self, version=None):
        """Get headers with optional API version"""
        headers = {'Content-Type': 'application/json'}
        
        if self.auth_token:
            headers['Authorization'] = f'Bearer {self.auth_token}'
            
        if version:
            # Different versioning strategies
            headers['API-Version'] = version  # Header-based versioning
            headers['Accept'] = f'application/vnd.api+json;version={version}'  # Accept header
            
        return headers

    @task(4)
    def test_version_header_strategy(self):
        """Test header-based API versioning"""
        version = random.choice(self.api_versions)
        
        # Create user with specific API version
        user_data = {
            "name": f"Test User {random.randint(100, 999)}",
            "email": f"test{random.randint(1000, 9999)}@example.com",
            "age": random.randint(18, 80)
        }
        
        # Add version-specific fields
        if version == "v1":
            user_data["phone"] = f"+1555{random.randint(1000000, 9999999)}"
        elif version == "v2":
            user_data.update({
                "phone_number": f"+1555{random.randint(1000000, 9999999)}",
                "preferences": {"newsletter": True, "sms": False}
            })
        elif version == "v3":
            user_data.update({
                "contact": {
                    "phone": f"+1555{random.randint(1000000, 9999999)}",
                    "preferred_method": "email"
                },
                "settings": {
                    "notifications": {"email": True, "push": True},
                    "privacy": {"profile_public": False}
                }
            })
        
        response = self.client.post('/api/users',
                                  json=user_data,
                                  headers=self._get_headers(version),
                                  name=f"create_user_{version}_header")
        
        if response.status_code in [200, 201]:
            try:
                created_user = response.json()
                user_id = created_user.get('id')
                if user_id:
                    self.test_data[f"user_{version}"] = user_id
                    self._test_version_response_format(version, user_id)
            except json.JSONDecodeError:
                pass

    def _test_version_response_format(self, version, user_id):
        """Test version-specific response formats"""
        response = self.client.get(f'/api/users/{user_id}',
                                 headers=self._get_headers(version),
                                 name=f"get_user_{version}_format")
        
        if response.status_code == 200:
            try:
                user_data = response.json()
                
                # Validate version-specific response structure
                if version == "v1":
                    # v1 might have flat structure
                    assert 'phone' in user_data, f"v1 should have 'phone' field"
                elif version == "v2":
                    # v2 might have phone_number instead of phone
                    assert 'phone_number' in user_data, f"v2 should have 'phone_number' field"
                    assert 'preferences' in user_data, f"v2 should have 'preferences' field"
                elif version == "v3":
                    # v3 might have nested contact structure
                    assert 'contact' in user_data, f"v3 should have 'contact' field"
                    assert 'settings' in user_data, f"v3 should have 'settings' field"
                    
            except (json.JSONDecodeError, AssertionError) as e:
                print(f"Version {version} response validation failed: {e}")

    @task(3)
    def test_url_path_versioning(self):
        """Test URL path-based API versioning"""
        version = random.choice(self.api_versions)
        
        # Test different endpoint structures per version
        product_data = {
            "name": f"Test Product {random.randint(100, 999)}",
            "price": round(random.uniform(10.00, 999.99), 2),
            "category": random.choice(["electronics", "clothing", "books"])
        }
        
        # Version-specific endpoint paths
        if version == "v1":
            endpoint = f'/api/{version}/products'
            product_data["description"] = "Simple description"
        elif version == "v2":
            endpoint = f'/api/{version}/products'
            product_data.update({
                "description": "Enhanced description",
                "tags": ["test", "product"],
                "metadata": {"source": "api_test"}
            })
        elif version == "v3":
            endpoint = f'/api/{version}/catalog/products'  # Different path structure
            product_data.update({
                "description": {
                    "short": "Brief description",
                    "long": "Detailed product description"
                },
                "taxonomy": {
                    "category": product_data["category"],
                    "subcategory": "test_subcategory"
                },
                "attributes": {"color": "blue", "size": "medium"}
            })
        
        response = self.client.post(endpoint,
                                  json=product_data,
                                  headers=self._get_headers(),
                                  name=f"create_product_{version}_path")

    @task(2)
    def test_version_compatibility(self):
        """Test backward compatibility between versions"""
        # Create data with v1 API
        v1_data = {
            "title": f"V1 Article {random.randint(100, 999)}",
            "content": "Article created with v1 API",
            "author": "Test Author",
            "published": True
        }
        
        v1_response = self.client.post('/api/v1/articles',
                                     json=v1_data,
                                     headers=self._get_headers(),
                                     name="create_article_v1")
        
        if v1_response.status_code in [200, 201]:
            try:
                v1_article = v1_response.json()
                article_id = v1_article.get('id')
                
                if article_id:
                    # Try to access same data with v2 API
                    v2_response = self.client.get(f'/api/v2/articles/{article_id}',
                                                headers=self._get_headers("v2"),
                                                name="get_article_v1_data_via_v2")
                    
                    # Try to update v1 data with v2 API
                    v2_update = {
                        "title": f"Updated via V2 - {random.randint(100, 999)}",
                        "content": "Updated content via v2 API",
                        "metadata": {"updated_via": "v2", "timestamp": int(time.time())}
                    }
                    
                    self.client.put(f'/api/v2/articles/{article_id}',
                                  json=v2_update,
                                  headers=self._get_headers("v2"),
                                  name="update_v1_article_via_v2")
            except json.JSONDecodeError:
                pass

    @task(1)
    def test_version_deprecation(self):
        """Test deprecated version handling"""
        # Test deprecated v1 endpoint
        deprecated_data = {
            "old_field": "value",
            "legacy_format": True
        }
        
        response = self.client.post('/api/v1/legacy-endpoint',
                                  json=deprecated_data,
                                  headers=self._get_headers("v1"),
                                  name="test_deprecated_v1_endpoint")
        
        # Check for deprecation warnings in headers
        if response.status_code == 200:
            deprecation_warning = response.headers.get('Deprecation')
            sunset_header = response.headers.get('Sunset')
            
            if deprecation_warning:
                print(f"Deprecation warning received: {deprecation_warning}")
            if sunset_header:
                print(f"Sunset date: {sunset_header}")

    def on_stop(self):
        """Cleanup test data across all versions"""
        for version in self.api_versions:
            user_id = self.test_data.get(f"user_{version}")
            if user_id:
                try:
                    self.client.delete(f'/api/users/{user_id}',
                                     headers=self._get_headers(version),
                                     name=f"cleanup_user_{version}")
                except:
                    pass

Key Testing Points

  1. Version Strategies: Header, path, query parameter, and Accept header versioning
  2. Backward Compatibility: Ensure older versions continue to work
  3. Response Formats: Validate version-specific response structures
  4. Deprecation Handling: Test deprecated version warnings and sunset dates
  5. Migration Support: Test data migration between versions
  6. Error Handling: Proper responses for unsupported versions

This guide provides comprehensive API versioning testing patterns for maintaining backward compatibility and smooth version transitions. 

Ready to run your test?
Run your test today with LoadForge.