In this article, Mikhail Golikov, the sole QA on a seven-team backend platform, explains how he turned a drawer full of unrun Postman collections into committable pytest suites, walks through the conversion request by request, and shows the folder-scoped command-line tool he built so one team’s pattern could scale to seven.
Auhor: Mikhail Golikov
We had a dozen Postman collections and not one of them ran on its own.
They were good collections. Someone had sat down and captured the endpoints, the headers, the example bodies, the auth flow. New engineers opened them to learn how a service behaved. When an API changed, somebody usually remembered to update the collection. Usually.
The problem was that the collections were documentation pretending to be tests. A person had to open Postman, pick the right environment, and click Send, request by request, reading each response with their own eyes. Nothing failed a build. Nothing ran at night. If an endpoint started returning a 500 on Tuesday, we found out when a human got around to clicking it, which on a seven-team backend platform with one QA is not a schedule you want to depend on.
I was that one QA. The platform was backend e-commerce, microservices, seven teams shipping into shared services. My job was to keep regression honest across all of it, and a stack of collections that only ran when I clicked them was not regression. It was a to-do list.
Why teams keep Postman collections but never run them in CI
It is easy to look at a folder of Postman collections and conclude the team has no API tests. That was not my situation, and it is probably not yours either. The requests existed. The expected responses existed. The auth was figured out. What was missing was the part that makes a test a test: it has to run without me, and it has to fail loudly when something breaks.
Postman is very good at the authoring half of that. You poke an endpoint, save the request, tweak the body, add a quick check in the Tests tab. It is the right tool for exploring an API and the wrong tool for guarding one, because guarding an API means running every check on every push, with no human in the loop. The collection sits in source control or in a shared workspace, and it slowly drifts away from the running service, because nothing forces the two back into agreement.
So the gap was narrow and specific. I did not need to design a test suite from scratch. I needed to take work that was already done, the requests and the assertions sitting inside those collection files, and move it into a runner that CI could call. pytest was the obvious target. We already ran pytest for everything else. The team read pytest output without translation. Adding a second framework just to run API checks would have been a tax nobody wanted to pay.
The collections were documentation pretending to be tests. The migration was not about writing tests. It was about making tests that already existed run without a human.
The naive route is to retype everything. Open a collection, read a request, write an httpx call in Python, copy the URL, copy the headers, translate the example assertion into an assert. Do that twelve collections deep and you will get most of the way and then quietly stop, because it is dull and the collections keep changing under you. I did some of it by hand at first. I stopped when I realized I was making transcription mistakes a script would never make.
The conversion approach: from v2.1 JSON to a committable module
A Postman Collection v2.1 file is just JSON. Once you stop seeing the Postman UI and start reading the file, the migration becomes a mapping exercise. Almost every piece of a collection has a natural home in a pytest project. Here is the mapping I settled on.
| Postman concept | pytest equivalent |
| Collection | A generated test module |
| Folder inside a collection | Prefix on the test name, and a filter target |
| Request | Test function |
| Method plus URL | An httpx call inside the test |
| Headers and auth | Header values rendered into the request |
| Postman environment variable ({{token}}) | os.environ.get(‘token’, ”) in the generated code |
| Base URL | Read from the BASE_URL environment variable |
| Test script status check (pm.response.to.have.status) | assert response.status_code == … |
| Test script JSON check (pm.expect(…)) | assert on the parsed JSON body |
| Pre-request script | A setup fixture, written by hand |
| Saved example response | Not migrated, left as reference |
Two rows in that table are honest about their limits, and I will come back to them, because pretending the mapping is total is how you end up with a tool people stop trusting.
The part worth dwelling on is the environment variables, because it is where secrets either leak or stay put. In Postman you reference a token as {{token}} and keep the real value in an environment. When the collection is exported, those references survive in the JSON. The converter turns each one into os.environ.get(‘token’, ”) in the generated Python, and reads the API host from BASE_URL the same way. So a request that used {{token}} in Postman becomes a header built from os.environ.get(‘token’, ”), and the committed test file carries no secret, only a lookup. The one caveat to be loud about: this only protects values you kept as Postman environment variables. If someone hardcoded a real token straight into a request, the tool has no way to know it is a secret, and it will carry through. Use environment variables in Postman and the generated tests inherit that hygiene. Skip them and no converter can save you.
The output itself is rendered through a Jinja2 template, which matters less for what it does and more for what it produces: a plain, readable pytest file you commit to the repo like any other. It is not a runtime that reads your collection on every run. It is a generator that emits Python you can open, read, edit, and review in a pull request. The collection is the input. The committed module is the artifact.
The tool: one command per collection
So I wrote postman2pytest. It is an open-source command-line tool, released under the MIT licence, published on PyPI, with its source on GitHub. It reads a Postman Collection v2.1 JSON file and writes a runnable pytest file. The point was never to be clever. The point was that the boring ninety percent should be free, so I could spend my attention on the ten percent that needs judgment.
It works as a generator, not a runtime. You run it once against a collection and it emits a plain pytest module that you commit to the repo: requests become test functions, headers and auth become rendered header values, Postman environment variables become os.environ.get(…) lookups, status checks become assert response.status_code == …, and the common test-script assertions, such as response time and JSON field equality, become real Python assert statements. What it cannot translate safely it leaves alone rather than guessing. The result is an ordinary test file you open, read, edit, and review in a pull request, not a black box that re-reads your collection on every run.
Why not just run the collection with newman?
It is a fair question, and one I expect from anyone who already lives in Postman. Postman ships newman, a command-line runner that executes a collection as-is, and for some teams that is enough. It was not enough for mine, for three reasons. Newman keeps the collection as the thing under test, so your real test logic stays in JavaScript inside the collection file, separate from the rest of your suite. It pulls a Node toolchain into a Python project that otherwise has none. And its results come in their own format, one more output the team has to learn to read next to pytest. Converting to pytest removes all three at once: the checks become native Python in the runner the team already uses, reviewable in a pull request, able to share fixtures with every other test, with no second language and no second runner to keep alive. Newman runs your collection. I wanted the checks to live in my code.
The workflow is short.
- Export the collection from Postman as Collection v2.1 JSON. This is built into Postman; there is nothing to install on that side.
- Install the tool from PyPI: pip install postman2pytest.
- Run it against the file: postman2pytest –collection my_api.json –out tests/test_api.py.
- Read the generated module. Set BASE_URL and any token variables in your environment, and add fixtures for anything the collection set up by hand.
- Run pytest tests/test_api.py -v. Watch what passes and what does not.
Step four is where a human earns their keep, and the tool is built to make step four short rather than to pretend it does not exist. The generated file is meant to be read and edited, not treated as a black box. It is the first eighty to ninety percent of a test suite, laid out the way a Python developer expects, so the remaining work is judgment and not typing.
The first time I ran it across our collections, the part that surprised me was not the time saved on writing. It was the bugs it surfaced. Several requests that everyone assumed were fine had quietly drifted: an endpoint that now returned a 404, a field that had been renamed, a response shape that no longer matched the saved example. They had drifted precisely because nobody was running them. The moment the collections became pytest functions in CI, the drift had nowhere to hide.
Folder filtering: scope control across a multi-team monorepo
A shared collection rarely belongs to one team. Ours grouped requests into Postman folders by area, and those areas mapped to the teams that owned them. If I converted a whole collection into one test file, every team’s CI job would run every other team’s checks, fail on services they did not own, and learn to ignore red builds. That is how a test suite dies.
The tool takes a folder name and generates tests only for that slice:
postman2pytest –collection shared.json –out tests/test_orders.py –filter-folder Orders
Each team points its own job at its own folder. The Orders job runs Orders tests. The Catalog job runs Catalog tests. The collection stays shared and singular, which is what keeps it honest as documentation, but the generated suites are scoped, which is what keeps each team’s build meaningful. A red build now means your slice broke, not someone else’s, and a red build that means something is a red build people fix.
What I learned scaling from one team to seven
I started on my own team’s collection. Export, convert, set the environment, run, commit. The pattern that worked for one collection was the same for the next, and the one after that. The tool did not change as the count went up. What changed was the convention around it.
The lesson that carried over from years of being the only tester on multi-team work is that the hard part is never the conversion. It is the agreement. For this to hold across seven teams, every team had to accept two rules. The collection is the source of truth, kept current in Postman by whoever owns the area. The generated tests are regenerated, not hand-edited, so a fix lands in the collection and flows out to the suite, instead of rotting in a Python file nobody syncs back. Where a team treated the generated file as just another hand-maintained test module, the two drifted apart within a sprint and we were back to documentation pretending to be tests, one layer down.
Folder filtering is what made the convention enforceable. Because each team owned a scoped slice, ownership was obvious and the blame for a stale test was never ambiguous. Tooling found the bugs. The convention decided whether anyone kept finding them.
Honest limits: where Postman authoring still wins
Here is the part the tooling content usually skips. A Postman collection and a pytest suite are not the same thing, and a converter cannot close every gap. If I oversell this, you run it once, hit the seam, and never trust the output again. So let me be exact about where the line is.
Postman test scripts are JavaScript. Simple, common patterns map cleanly: a status-code check, a check that a JSON field equals a value, a check that a field exists. Those translate with confidence. But a collection can contain arbitrary JavaScript in its test and pre-request scripts, loops, custom helper functions, calls to Postman’s sandbox API that have no Python equivalent. The tool does not try to transpile arbitrary JavaScript, because a converter that is right eighty percent of the time and silently wrong the other twenty is worse than no converter at all. Where a script is too custom to map safely, the honest move is to leave it for the human rather than to guess.
Chained requests are the other place to slow down. Postman flows often pass a value from one response into the next request, a token, an id, a cursor. That sequencing is real test logic, and it deserves to be written deliberately in Python with fixtures, not machine-translated. The tool gives you the individual requests as a starting point. Stitching them into a flow is your job, and it should be.
And for a whole class of work, Postman authoring simply stays the better tool. Exploring a new endpoint, reproducing a bug by hand, sharing a one-off request with a teammate, poking at an API you do not yet understand: that is what Postman is for, and converting it to code would only slow you down. Code-first wins for the checks you want to run forever without thinking about them. Postman wins for the thinking. The migration was never Postman versus pytest. Each tool keeps the job it is good at, and the durable half gets a home in CI.
None of this is a flaw to apologize for. It is the correct boundary. The mechanical work is mechanical, so a tool does it. The work that needs a tester’s judgment stays with the tester, where it belongs.
What I would tell the next sole QA
If you are the only tester staring at a folder of collections that only run when you click them, the question is not whether you have tests. You have most of one. The question is what it would take to make the tests you already have run without you, in the runner your team already reads, scoped so each team owns its slice, and with a clear line between what a script can convert and what still needs your hands.
Map that boundary before you write a line. The mechanical part is free now. Spend your attention on the part that is not, and on the agreement that keeps seven teams pointing at the same source of truth.
postman2pytest is open source and on PyPI. If your collections look like mine did, it will take you the same afternoon it took me.
References
- postman2pytest on PyPI: https://pypi.org/project/postman2pytest/
- postman2pytest source on GitHub: https://github.com/golikovichev/postman2pytest
- Postman Collection Format v2.1 schema: https://schema.postman.com/
- “Turn Your Postman Collection Into Pytest Tests With One Command” (HackerNoon): https://hackernoon.com/u/golikovichev
About the Author
Mikhail Golikov is a QA automation engineer based in Hove, UK. He has more than five years in QA, most recently as the sole tester on a multi-team backend e-commerce platform, where he builds regression suites and open-source testing tools for Python. He maintains postman2pytest and several other pytest plugins under the handle golikovichev.



Leave a Reply