LearnCen Docs
Guides 6 min read

pytest API Testing Environment

Enterprise REST API testing with pytest, requests, httpx, Pydantic validation, JSON Schema, Allure reporting, parallel execution, and mocking.

What's Inside

Tool Version Purpose
Python 3.x Runtime
pytest Latest Test framework with fixtures, markers, parametrize
requests Latest Synchronous HTTP client
httpx Latest Async HTTP client (modern alternative to requests)
Pydantic Latest Response model validation (type-safe)
jsonschema Latest JSON Schema contract validation
pytest-html Latest HTML test reports
pytest-xdist Latest Parallel test execution
pytest-ordering Latest Test execution ordering
pytest-env Latest Environment variable management
pytest-timeout Latest Test timeout enforcement
pytest-cov Latest Code coverage reporting
allure-pytest Latest Allure report integration
Allure CLI Latest Rich HTML test reporting with trends
Faker Latest Generate realistic test data
responses Latest Mock requests library calls
respx Latest Mock httpx calls
cerberus Latest Schema validation (alternative)
python-dotenv Latest .env file loading
Java JRE 17 Required by Allure CLI

Quick Start

# Run all API tests
api-run

# Run smoke tests only
api-smoke

# Run tests by marker
api-tag crud

# Run with Allure report
api-allure

# Run in parallel (4 workers)
api-parallel 4

# Show all commands
api-help

Helper Commands

Test Execution

Command What it does
api-run [file] Run all tests (or specific file)
api-smoke Run @smoke marked tests
api-regression Run @regression marked tests
api-tag <marker> Run tests by marker (crud, auth, schema, performance)
api-parallel N Run with N parallel workers (pytest-xdist)

Reports

Command What it does
api-allure Run tests + generate and serve Allure report
api-open-report Serve existing Allure report on port 9325

Utilities

Command What it does
api-help Show all available commands

Project Structure

workspace/
├── tests/
│   ├── api/
│   │   ├── test_get_requests.py       # GET endpoint tests
│   │   ├── test_crud.py               # POST/PUT/PATCH/DELETE tests
│   │   ├── test_auth.py               # Authentication tests
│   │   ├── test_schema_validation.py  # JSON Schema + Pydantic validation
│   │   └── test_data_driven.py        # Parametrized data-driven tests
│   └── schemas/
│       └── post_schema.json           # JSON Schema definitions
├── models/
│   └── post.py                        # Pydantic response models
├── utils/
│   └── api_client.py                  # Reusable API client class
├── config/                            # Environment configurations
├── conftest.py                        # Shared fixtures (session, base_url, auth)
├── pytest.ini                         # pytest configuration + markers
├── .env                               # Environment variables (BASE_URL, API_TIMEOUT)
└── exercises/                         # 8 guided lab exercises

Test Markers

@pytest.mark.smoke          # Quick validation tests
@pytest.mark.regression     # Full regression suite
@pytest.mark.crud           # CRUD operation tests
@pytest.mark.auth           # Authentication tests
@pytest.mark.schema         # JSON schema validation tests
@pytest.mark.performance    # Response time tests

Run by marker:

api-tag smoke           # Quick CI checks
api-tag crud            # Only CRUD tests
api-tag "smoke or auth" # Combined markers

Fixtures (conftest.py)

The workspace comes with shared fixtures ready to use:

@pytest.fixture(scope="session")
def base_url():
    """Base URL loaded from .env"""
    return "https://jsonplaceholder.typicode.com"

@pytest.fixture(scope="session")
def session():
    """Reusable requests.Session with default headers."""
    s = requests.Session()
    s.headers.update({"Content-Type": "application/json"})
    return s

@pytest.fixture
def auth_headers():
    """Bearer token headers (replace with real auth flow)."""
    return {"Authorization": "Bearer test-token-123"}

Writing API Tests

GET Request

import pytest
from models.post import Post

@pytest.mark.smoke
def test_get_posts(session, base_url):
    resp = session.get(f"{base_url}/posts")
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) > 0
    # Validate with Pydantic
    post = Post(**data[0])
    assert post.userId > 0

POST Request (Create)

@pytest.mark.crud
def test_create_post(session, base_url):
    payload = {"title": "New Post", "body": "Content", "userId": 1}
    resp = session.post(f"{base_url}/posts", json=payload)
    assert resp.status_code == 201
    assert resp.json()["title"] == "New Post"

Data-Driven (Parametrize)

@pytest.mark.parametrize("user_id", [1, 2, 3, 4, 5])
def test_posts_by_user(session, base_url, user_id):
    resp = session.get(f"{base_url}/posts", params={"userId": user_id})
    assert resp.status_code == 200
    assert all(p["userId"] == user_id for p in resp.json())

JSON Schema Validation

import jsonschema

POST_SCHEMA = {
    "type": "object",
    "required": ["userId", "id", "title", "body"],
    "properties": {
        "userId": {"type": "integer"},
        "id": {"type": "integer"},
        "title": {"type": "string", "minLength": 1},
        "body": {"type": "string", "minLength": 1},
    },
}

@pytest.mark.schema
def test_post_schema(session, base_url):
    resp = session.get(f"{base_url}/posts/1")
    jsonschema.validate(instance=resp.json(), schema=POST_SCHEMA)

Pydantic Model Validation

from pydantic import BaseModel, Field

class Post(BaseModel):
    userId: int
    id: int
    title: str = Field(min_length=1)
    body: str = Field(min_length=1)

def test_response_model(session, base_url):
    resp = session.get(f"{base_url}/posts/1")
    post = Post(**resp.json())  # Raises ValidationError if invalid
    assert post.id == 1

Mocking with responses

import responses

@responses.activate
def test_mocked_api():
    responses.add(responses.GET, "https://api.example.com/users",
        json=[{"id": 1, "name": "Test"}], status=200)
    resp = requests.get("https://api.example.com/users")
    assert resp.status_code == 200

Performance Testing

@pytest.mark.performance
def test_response_time(session, base_url):
    resp = session.get(f"{base_url}/posts")
    assert resp.elapsed.total_seconds() < 2.0

Reusable API Client

The utils/api_client.py provides a production-grade client:

from utils.api_client import APIClient

def test_with_client():
    client = APIClient(token="my-token")
    resp = client.get("/posts")
    assert resp.status_code == 200

Features: base URL management, auth headers, extensible for retry logic and logging.

Async Testing (httpx)

For concurrent API testing:

import httpx
import pytest

@pytest.mark.asyncio
async def test_concurrent_requests():
    async with httpx.AsyncClient(base_url="https://jsonplaceholder.typicode.com") as client:
        responses = await asyncio.gather(
            client.get("/posts/1"),
            client.get("/posts/2"),
            client.get("/posts/3"),
        )
        assert all(r.status_code == 200 for r in responses)

Parallel Execution

Speed up large test suites:

api-parallel 4    # 4 workers
api-parallel 8    # 8 workers (for large suites)

Uses pytest-xdist to distribute tests across CPU cores.

Allure Reporting

api-allure
# Generates rich HTML report served on port 9325

Enhance reports with decorators:

import allure

@allure.epic("REST API")
@allure.feature("Posts")
@allure.story("Create post")
@allure.severity(allure.severity_level.CRITICAL)
def test_create_post(session, base_url):
    ...
    allure.attach(resp.text, "Response", allure.attachment_type.JSON)

Environment Configuration

# .env (loaded automatically by conftest.py)
BASE_URL=https://jsonplaceholder.typicode.com
API_TIMEOUT=30

Override at runtime:

BASE_URL=https://staging.myapi.com api-run

Exercises

8 guided lab exercises included:

  1. pytest Fundamentals — fixtures, markers, assertions
  2. CRUD Operations — POST/PUT/PATCH/DELETE lifecycle
  3. Data-Driven Testing — @parametrize with multiple datasets
  4. JSON Schema Validation — contract testing with jsonschema + Pydantic
  5. Mocking — responses/respx for isolated testing
  6. API Client Pattern — reusable client with retry and logging
  7. Allure Reporting — decorators, attachments, severity levels
  8. CI/CD Integration — parallel execution, coverage, pipeline configs

VS Code Extensions (Pre-installed)

Extension Purpose
Python Language support, debugging, testing

Keyboard shortcuts:

  • Ctrl+Shift+B — Run tests + Allure report (default build task)
  • Ctrl+Shift+T — Run all API tests (default test task)

Tips

  • Use api-run -k "test_create" to run tests matching a name pattern
  • Use api-run --lf to re-run only last-failed tests
  • Use api-run --cov=. --cov-report=html for coverage reports
  • Pydantic models catch response schema drift automatically — use them for contract testing
  • responses library lets you test error handling without hitting real APIs
  • The session fixture is scoped to session — one TCP connection for all tests (faster)

Security

  • Root access is restricted. Use sudo apt install <package> for system packages.
  • pip, python3, npm are whitelisted for sudo access.
  • Python virtual environment is at /opt/learncen-venv/bin/python3.