py4u guide

Automated API Testing in Python with Pytest

API testing involves validating the functionality, reliability, performance, and security of APIs. Unlike UI testing, which focuses on user interfaces, API testing directly interacts with the backend logic by sending requests (e.g., GET, POST, PUT) and verifying responses. Key goals of API testing include: - Ensuring endpoints return correct status codes (e.g., 200 OK, 404 Not Found). - Validating response data structure (e.g., required fields, data types). - Testing edge cases (e.g., invalid inputs, authentication failures). - Ensuring performance (e.g., response time < 1 second).

APIs (Application Programming Interfaces) are the backbone of modern software systems, enabling communication between applications, services, and databases. Ensuring APIs work reliably, efficiently, and securely is critical for delivering high-quality software. Manual API testing, however, is time-consuming, error-prone, and难以扩展 as systems grow.

Automated API testing solves these challenges by allowing developers and QA engineers to write scripts that validate API behavior, catch regressions, and integrate seamlessly into CI/CD pipelines. In this blog, we’ll explore how to implement automated API testing in Python using Pytest—a powerful, flexible testing framework known for its simplicity and scalability.

Table of Contents

  1. Introduction to API Testing
  2. Why Pytest for API Testing?
  3. Prerequisites
  4. Setting Up the Testing Environment
  5. Understanding the Test API
  6. Writing Your First API Test with Pytest
  7. Advanced API Testing Scenarios
  8. Best Practices for API Testing with Pytest
  9. Conclusion
  10. References

Why Pytest for API Testing?

Pytest is a popular Python testing framework with several advantages for API testing:

  • Simple Syntax: Write tests as plain Python functions with assert statements (no boilerplate like unittest.TestCase).
  • Powerful Fixtures: Reuse setup/teardown logic (e.g., creating test data, authenticating) across tests.
  • Parametrization: Test multiple inputs/scenarios with @pytest.mark.parametrize.
  • Rich Ecosystem: Plugins like pytest-html (for reports), requests-mock (for mocking), and jsonschema (for schema validation) extend functionality.
  • CI/CD Integration: Seamlessly runs in pipelines (GitHub Actions, GitLab CI) to catch issues early.

Prerequisites

Before starting, ensure you have:

  • Python 3.7+ installed (download here).
  • Basic knowledge of Python and HTTP concepts (methods, status codes, JSON).
  • Familiarity with APIs (e.g., REST).

Setting Up the Testing Environment

Let’s set up a virtual environment and install dependencies:

Step 1: Create a Virtual Environment

Isolate project dependencies using a virtual environment:

# Create a virtual environment  
python -m venv venv  

# Activate it (Linux/macOS)  
source venv/bin/activate  

# Activate it (Windows)  
venv\Scripts\activate  

Step 2: Install Dependencies

Install pytest (testing framework) and requests (to send HTTP requests):

pip install pytest requests  

Understanding the Test API

We’ll use JSONPlaceholder—a free, public API for testing—to demonstrate concepts. It mimics a blog platform with endpoints for posts, users, and comments.

Key endpoints:

  • GET /posts: Fetch all posts (returns 100 sample posts).
  • GET /posts/{id}: Fetch a specific post by ID.
  • POST /posts: Create a new post (simulated; no data is persisted).
  • PUT /posts/{id}: Update a post (simulated).
  • DELETE /posts/{id}: Delete a post (simulated).

Example response for GET /posts/1:

{  
  "userId": 1,  
  "id": 1,  
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",  
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"  
}  

Writing Your First API Test with Pytest

Let’s start with a simple test to validate a GET request.

Step 1: Create a Test File

Create a file named test_api.py (Pytest auto-discovers files starting with test_).

Step 2: Test 1 – Validate Status Code

Write a test to ensure the /posts endpoint returns a 200 OK status code:

# test_api.py  
import pytest  
import requests  

def test_get_posts_status_code():  
    # Send GET request to /posts  
    response = requests.get("https://jsonplaceholder.typicode.com/posts")  
    
    # Assert the status code is 200  
    assert response.status_code == 200, "Expected status code 200"  

Step 3: Run the Test

Execute the test with:

pytest test_api.py -v  

Output:

collected 1 item  

test_api.py::test_get_posts_status_code PASSED  

The -v flag enables verbose mode to see test names.

Step 4: Test 2 – Validate Response Structure

Next, ensure the response contains expected fields (e.g., id, title). We’ll check the first post in the response:

def test_get_posts_response_structure():  
    response = requests.get("https://jsonplaceholder.typicode.com/posts")  
    posts = response.json()  # Parse JSON response  
    
    # Ensure the response is a list (even if empty)  
    assert isinstance(posts, list), "Response should be a list of posts"  
    
    # Check the first post has required fields  
    if posts:  # Handle empty response (unlikely for JSONPlaceholder)  
        first_post = posts[0]  
        required_fields = ["userId", "id", "title", "body"]  
        for field in required_fields:  
            assert field in first_post, f"Missing required field: {field}"  

Run Both Tests

pytest test_api.py -v  

Output:

collected 2 items  

test_api.py::test_get_posts_status_code PASSED  
test_api.py::test_get_posts_response_structure PASSED  

Advanced API Testing Scenarios

7.1 Authentication & Authorization

Many APIs require authentication (e.g., API keys, OAuth tokens). Let’s test the GitHub API, which uses personal access tokens (PATs) for authentication.

Step 1: Generate a GitHub PAT

Step 2: Test Authenticated Request

Store the token in an environment variable (never hardcode secrets!):

# Linux/macOS  
export GITHUB_TOKEN="your_pat_here"  

# Windows (Command Prompt)  
set GITHUB_TOKEN=your_pat_here  

Write a test to fetch your GitHub user data:

import os  

def test_github_authenticated_request():  
    headers = {  
        "Authorization": f"token {os.getenv('GITHUB_TOKEN')}"  
    }  
    response = requests.get("https://api.github.com/user", headers=headers)  
    
    # Assert 200 OK (authenticated) or 401 Unauthorized (invalid token)  
    assert response.status_code == 200, "Authentication failed"  

7.2 Data-Driven Testing with Parametrization

Use @pytest.mark.parametrize to test multiple endpoints or inputs in a single test. For example, validate status codes for /posts, /users, and /comments:

import pytest  

@pytest.mark.parametrize("endpoint, expected_status", [  
    ("/posts", 200),  
    ("/users", 200),  
    ("/comments", 200),  
    ("/invalid-endpoint", 404)  # Test invalid endpoint  
])  
def test_multiple_endpoints_status_codes(endpoint, expected_status):  
    base_url = "https://jsonplaceholder.typicode.com"  
    response = requests.get(f"{base_url}{endpoint}")  
    assert response.status_code == expected_status, f"Failed for {endpoint}"  

Run with:

pytest test_api.py -v  

Output (truncated):

test_api.py::test_multiple_endpoints_status_codes[/posts-200] PASSED  
test_api.py::test_multiple_endpoints_status_codes[/users-200] PASSED  
test_api.py::test_multiple_endpoints_status_codes[/comments-200] PASSED  
test_api.py::test_multiple_endpoints_status_codes[/invalid-endpoint-404] PASSED  

7.3 Response Validation (Status Codes, Schema, and Performance)

For robust validation, check:

  • Status codes (as shown earlier).
  • JSON Schema: Ensure responses match a predefined schema (use the jsonschema library).
  • Response Time: Ensure APIs are performant.

Validate JSON Schema

Install jsonschema:

pip install jsonschema  

Define a schema for a post and validate the response:

from jsonschema import validate  

# Define the expected schema for a post  
POST_SCHEMA = {  
    "type": "object",  
    "properties": {  
        "userId": {"type": "integer"},  
        "id": {"type": "integer"},  
        "title": {"type": "string"},  
        "body": {"type": "string"}  
    },  
    "required": ["userId", "id", "title", "body"]  # Mandatory fields  
}  

def test_post_schema_validation():  
    response = requests.get("https://jsonplaceholder.typicode.com/posts/1")  
    post = response.json()  
    
    # Validate the post against the schema  
    validate(instance=post, schema=POST_SCHEMA)  

Validate Response Time

Ensure the API responds within 1 second:

def test_response_time():  
    response = requests.get("https://jsonplaceholder.typicode.com/posts")  
    response_time = response.elapsed.total_seconds()  # Time in seconds  
    assert response_time < 1, f"Response time too slow: {response_time}s"  

7.4 Error Handling

Test edge cases like invalid methods or missing resources. For example, ensure a POST request to a read-only endpoint returns 405 Method Not Allowed:

def test_invalid_method():  
    # Attempt to POST to /posts/1 (which only allows GET/PUT/DELETE)  
    response = requests.post("https://jsonplaceholder.typicode.com/posts/1")  
    assert response.status_code == 405, "Expected 405 Method Not Allowed"  

Best Practices for API Testing with Pytest

  1. Test Isolation: Each test should run independently (no shared state between tests). Use Pytest fixtures for setup/teardown:

    @pytest.fixture  
    def base_url():  
        return "https://jsonplaceholder.typicode.com"  
    
    def test_get_posts(base_url):  # Reuse base_url across tests  
        response = requests.get(f"{base_url}/posts")  
        assert response.status_code == 200  
  2. Mock External Dependencies: Avoid relying on real APIs for unit tests (they may be slow/unreliable). Use requests-mock to mock responses:

    pip install requests-mock  
    def test_mocked_api(requests_mock):  
        # Mock GET /posts to return a custom response  
        requests_mock.get("https://jsonplaceholder.typicode.com/posts", json=[{"id": 1}])  
        response = requests.get("https://jsonplaceholder.typicode.com/posts")  
        assert response.json() == [{"id": 1}]  
  3. Generate Reports: Use pytest-html to create detailed HTML reports:

    pip install pytest-html  
    pytest test_api.py --html=report.html  
  4. Parallelize Tests: Speed up test suites with pytest-xdist (runs tests in parallel):

    pip install pytest-xdist  
    pytest -n auto  # Uses all CPU cores  
  5. Use Descriptive Test Names: Names like test_post_with_missing_title_returns_400 make failures easier to debug.

Conclusion

Automated API testing with Pytest ensures your APIs are reliable, performant, and secure. By following the steps in this guide, you can write maintainable tests that integrate seamlessly into your development workflow. Start with simple status code checks, then layer in response validation, authentication, and data-driven testing to build a robust test suite.

References