A Postman collection is a great place to explore an API. But it's a poor place to keep your tests.

Most teams find this out the slow way. Someone exports the collection, converts the requests into test code once, and moves on. Six months later the tests are red, nobody trusts them, and they get skipped in the pipeline. The conversion was never the hard part. Keeping the suite alive is.

This tutorial takes you from a Postman collection to a pytest suite that still passes next quarter. First we'll look at why converted tests rot, then at four principles that keep them alive. The examples stay small, so you can try them on your own collection today.

Table of Contents

Before You Start

To follow along you will need:

  • Python 3.10 or newer, with pytest and httpx installed (pip install pytest httpx).

  • A Postman collection you want to convert, with its environment (the base URL and token).

  • Basic pytest knowledge: how fixtures work and how to run pytest from the command line.

  • A GitHub repository if you want to try the continuous integration step. You can skip that part and still follow the rest.

Diagram: a Postman collection converts into a generated pytest suite (the easy step), which then becomes a maintainable suite through four practices: environment in fixtures, asserting the contract not just the status code, independent tests, and running in CI on every push.

The diagram shows the two parts of the job. On the left, a Postman collection (its requests and environment) is converted into a generated pytest suite, which is the first draft. That conversion is the easy step.

The work is the maintainability layer on the right, which turns that first draft into a suite you can rely on: the environment lives in fixtures instead of being hardcoded, tests assert the response contract rather than just a 200 status, each test is independent, and the suite runs in continuous integration on every push.

Why Converted Tests Go Stale

When you convert Postman requests one to one, you tend to inherit four habits that feel fine on day one and hurt by day thirty:

  • The base URL and the token are hardcoded into every test, so moving from staging to production means a find and replace.

  • The tests run in a fixed order because request two depends on a value request one set, so a single failure cascades.

  • The only assertion is that the status code was 200, which passes even when the response body is wrong.

  • Setup is copied into every test, so one change to how you authenticate means editing twenty files.

Every one of these is a maintenance problem, and together they're why the suite gets abandoned. Here's how to avoid each one.

Principle 1: Keep the Environment Out of the Tests

A Postman collection carries its environment in a separate file: base URL, tokens, and other variables. Do the same in pytest. Read those values once, in a fixture, and let every test ask for them.

# conftest.py
import os

import httpx
import pytest


@pytest.fixture(scope="session")
def base_url():
    return os.environ["API_BASE_URL"]


@pytest.fixture(scope="session")
def auth_headers():
    return {"Authorization": f"Bearer {os.environ['API_TOKEN']}"}


@pytest.fixture()
def http():
    with httpx.Client(timeout=10) as client:
        yield client

Now a test never mentions a URL or a token directly:

def test_get_user(base_url, auth_headers, http):
    response = http.get(f"{base_url}/users/1", headers=auth_headers)
    assert response.status_code == 200

Switching from staging to production is now one environment variable, not a search across the whole suite.

Principle 2: Assert on the Contract, Not Just the Status Code

A status of 200 tells you the server answered. It doesn't tell you the answer was correct. The most common reason a broken API ships is that every test only checked the status.

Assert on the shape of the response and the fields your callers depend on.

def test_user_shape(base_url, auth_headers, http):
    response = http.get(f"{base_url}/users/1", headers=auth_headers)

    assert response.status_code == 200
    body = response.json()
    assert set(body) >= {"id", "email", "created_at"}
    assert isinstance(body["id"], int)
    assert "@" in body["email"]

You don't need a strict schema for every endpoint. Even a few checks on the fields that matter will catch a whole class of regressions that a status check waves through.

Principle 3: Make Each Test Stand on its Own

In Postman, it's normal for one request to feed the next. In a test suite, that coupling is a trap: reorder the tests, run one in isolation, or lose the first request, and the rest fall over.

Give each test the state it needs. If a test needs a user, it creates one.

def test_delete_user(base_url, auth_headers, http):
    created = http.post(
        f"{base_url}/users",
        headers=auth_headers,
        json={"email": "temp@example.com"},
    )
    user_id = created.json()["id"]

    response = http.delete(f"{base_url}/users/{user_id}", headers=auth_headers)
    assert response.status_code == 204

Independent tests can run in any order and in parallel, and a failure points at one thing instead of a chain.

Principle 4: Put the Suite in Continuous Integration on Day One

A test suite that only runs on your laptop drifts out of date the moment you stop looking at it. Wire it into your pipeline before you write the second test, so every push has to keep it green.

# .github/workflows/tests.yml
name: API tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest -v
        env:
          API_BASE_URL: ${{ secrets.API_BASE_URL }}
          API_TOKEN: ${{ secrets.API_TOKEN }}

Once this is in place, a test that breaks is a conversation on a pull request, not a surprise in production.

Let a Tool Do the Mechanical Part

Everything above is the part worth your attention. Turning each request into a first draft of a test is mechanical, and mechanical work is worth automating.

I maintain an open-source tool for exactly this step called postman2pytest. It reads a Postman collection and writes a runnable pytest file, so you start from generated tests and spend your time on the maintainability layer rather than on the boilerplate. When the collection changes, you regenerate rather than hand-patching the drift.

You can find it here: https://github.com/golikovichev/postman2pytest

Wrapping Up

Converting a Postman collection into tests is easy. Keeping those tests trustworthy is the real skill, and it comes down to a few habits: keep the environment out of the tests, assert on the contract and not just the status code, make each test independent, and run everything in continuous integration from the start.

Do that, and the suite you generate this week will still be the suite you rely on next year.