API Testing with JSON: Tools and Techniques

Testing REST APIs is crucial for reliable applications. Since most modern APIs use JSON for data exchange, mastering JSON validation and testing techniques is essential. This guide covers everything from basic validation to advanced testing strategies.

Why JSON API Testing Matters

Essential Testing Tools

1. curl - Command Line Testing

curl is perfect for quick API tests and automation:

# GET request
curl -X GET "https://api.example.com/users" \
  -H "Accept: application/json"

# POST request with JSON data
curl -X POST "https://api.example.com/users" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com"
  }'

# Pretty print JSON response
curl -s "https://api.example.com/users" | python -m json.tool

# Save response to file
curl "https://api.example.com/users" -o response.json

2. Postman - GUI Testing

Postman provides a user-friendly interface for API testing:

Postman Test Script Example:

// Test status code
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

// Test JSON structure
pm.test("Response has required fields", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData).to.have.property('id');
    pm.expect(jsonData).to.have.property('name');
    pm.expect(jsonData).to.have.property('email');
});

// Test data types
pm.test("ID is a number", function () {
    const jsonData = pm.response.json();
    pm.expect(jsonData.id).to.be.a('number');
});

3. Python requests - Programmatic Testing

Python requests library is excellent for automated testing:

import requests
import json

def test_api_endpoint():
    # Test GET request
    response = requests.get('https://api.example.com/users/1')
    
    # Basic assertions
    assert response.status_code == 200
    assert response.headers['content-type'] == 'application/json'
    
    # Parse JSON response
    data = response.json()
    
    # Validate JSON structure
    required_fields = ['id', 'name', 'email']
    for field in required_fields:
        assert field in data, f"Missing required field: {field}"
    
    # Validate data types
    assert isinstance(data['id'], int)
    assert isinstance(data['name'], str)
    assert isinstance(data['email'], str)
    
    return data

# Test POST request
def test_create_user():
    user_data = {
        "name": "Alice Smith",
        "email": "alice@example.com"
    }
    
    response = requests.post(
        'https://api.example.com/users',
        json=user_data,
        headers={'Content-Type': 'application/json'}
    )
    
    assert response.status_code == 201
    created_user = response.json()
    
    # Validate created user
    assert created_user['name'] == user_data['name']
    assert created_user['email'] == user_data['email']
    assert 'id' in created_user
    
    return created_user

JSON Validation Techniques

1. Schema Validation

Use JSON Schema to validate response structure:

import jsonschema
import requests

# Define expected schema
user_schema = {
    "type": "object",
    "properties": {
        "id": {"type": "integer"},
        "name": {"type": "string", "minLength": 1},
        "email": {"type": "string", "format": "email"},
        "age": {"type": "integer", "minimum": 0},
        "active": {"type": "boolean"}
    },
    "required": ["id", "name", "email"]
}

def validate_user_response(response_data):
    try:
        jsonschema.validate(response_data, user_schema)
        print("✅ JSON schema validation passed")
        return True
    except jsonschema.ValidationError as e:
        print(f"❌ Schema validation failed: {e.message}")
        return False

# Test with API response
response = requests.get('https://api.example.com/users/1')
user_data = response.json()
validate_user_response(user_data)

2. Custom Validation Functions

def validate_api_response(response, expected_fields=None, expected_types=None):
    """
    Comprehensive API response validation
    """
    # Check status code
    if not 200 <= response.status_code < 300:
        raise AssertionError(f"Unexpected status code: {response.status_code}")
    
    # Check content type
    content_type = response.headers.get('content-type', '')
    if 'application/json' not in content_type:
        raise AssertionError(f"Expected JSON, got: {content_type}")
    
    # Parse JSON
    try:
        data = response.json()
    except ValueError as e:
        raise AssertionError(f"Invalid JSON response: {e}")
    
    # Validate fields
    if expected_fields:
        missing_fields = set(expected_fields) - set(data.keys())
        if missing_fields:
            raise AssertionError(f"Missing fields: {missing_fields}")
    
    # Validate types
    if expected_types:
        for field, expected_type in expected_types.items():
            if field in data and not isinstance(data[field], expected_type):
                raise AssertionError(
                    f"Field '{field}' should be {expected_type.__name__}, "
                    f"got {type(data[field]).__name__}"
                )
    
    return data

# Usage example
response = requests.get('https://api.example.com/users/1')
user_data = validate_api_response(
    response,
    expected_fields=['id', 'name', 'email'],
    expected_types={'id': int, 'name': str, 'email': str}
)

Testing Different Scenarios

1. Error Response Testing

def test_error_responses():
    # Test 404 - Not Found
    response = requests.get('https://api.example.com/users/99999')
    assert response.status_code == 404
    
    error_data = response.json()
    assert 'error' in error_data
    assert 'message' in error_data
    
    # Test 400 - Bad Request
    invalid_data = {"email": "invalid-email"}
    response = requests.post(
        'https://api.example.com/users',
        json=invalid_data
    )
    assert response.status_code == 400
    
    error_data = response.json()
    assert 'validation_errors' in error_data

# Test authentication errors
def test_auth_errors():
    # Test without token
    response = requests.get('https://api.example.com/protected')
    assert response.status_code == 401
    
    # Test with invalid token
    headers = {'Authorization': 'Bearer invalid-token'}
    response = requests.get('https://api.example.com/protected', headers=headers)
    assert response.status_code == 401

2. Performance Testing

import time

def test_response_time():
    start_time = time.time()
    response = requests.get('https://api.example.com/users')
    end_time = time.time()
    
    response_time = end_time - start_time
    
    # Assert response time is under 2 seconds
    assert response_time < 2.0, f"Response too slow: {response_time:.2f}s"
    
    # Check response size
    content_length = len(response.content)
    assert content_length < 1024 * 1024, f"Response too large: {content_length} bytes"

Automated Testing with pytest

import pytest
import requests

class TestUserAPI:
    base_url = "https://api.example.com"
    
    def test_get_users(self):
        response = requests.get(f"{self.base_url}/users")
        assert response.status_code == 200
        
        users = response.json()
        assert isinstance(users, list)
        
        if users:  # If users exist
            user = users[0]
            assert 'id' in user
            assert 'name' in user
            assert 'email' in user
    
    def test_create_user(self):
        user_data = {
            "name": "Test User",
            "email": "test@example.com"
        }
        
        response = requests.post(
            f"{self.base_url}/users",
            json=user_data
        )
        assert response.status_code == 201
        
        created_user = response.json()
        assert created_user['name'] == user_data['name']
        assert created_user['email'] == user_data['email']
    
    @pytest.mark.parametrize("invalid_email", [
        "invalid-email",
        "@example.com",
        "test@",
        ""
    ])
    def test_invalid_email_validation(self, invalid_email):
        user_data = {
            "name": "Test User",
            "email": invalid_email
        }
        
        response = requests.post(
            f"{self.base_url}/users",
            json=user_data
        )
        assert response.status_code == 400

JSON Testing Best Practices

1. Validate Response Structure

2. Test Edge Cases

3. Security Testing

Online JSON Testing Tools

For quick JSON validation and testing, use these online tools:

Conclusion

Effective API testing with JSON requires a combination of tools, techniques, and best practices. Start with basic validation, add schema checking, and gradually build comprehensive test suites. Remember to test both success and error scenarios, and always validate the JSON structure and data types.

Regular API testing ensures your applications remain reliable, secure, and performant as they evolve.