<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Imran Al Munyeem</title>
    <description>The latest articles on DEV Community by Imran Al Munyeem (@imranalmunyeem).</description>
    <link>https://dev.to/imranalmunyeem</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4028756%2F2639130a-5e5f-4c62-9ba7-483eb6632df4.png</url>
      <title>DEV Community: Imran Al Munyeem</title>
      <link>https://dev.to/imranalmunyeem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imranalmunyeem"/>
    <language>en</language>
    <item>
      <title>Create, Then Verify: Turning Postman Requests into Real Workflow Tests</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 31 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/create-then-verify-turning-postman-requests-into-real-workflow-tests-43h5</link>
      <guid>https://dev.to/imranalmunyeem/create-then-verify-turning-postman-requests-into-real-workflow-tests-43h5</guid>
      <description>&lt;p&gt;A DELETE endpoint that returns &lt;code&gt;204 No Content&lt;/code&gt; and deletes nothing will pass every single-request test you write.&lt;/p&gt;

&lt;p&gt;The status code is right. The response time is fine. The body is empty, as specified. Your suite is green — and the record is still in the database, because the only way to &lt;em&gt;know&lt;/em&gt; deletion worked is to ask again: a follow-up GET that must return 404.&lt;/p&gt;

&lt;p&gt;That's the behaviour layer of API testing, and it can't be reached one request at a time. It needs &lt;strong&gt;chained requests&lt;/strong&gt; — a sequence where each step feeds the next, testing the resource's whole lifecycle. In Postman the bridge between steps is one line of script. Here's the full pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bridge: capture, then reference
&lt;/h2&gt;

&lt;p&gt;Everything hangs on one move — capture a value from a response, use it in the next request's URL or body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request 1 — POST /users&lt;/strong&gt; (Scripts → Post-response):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newUserId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Request 2 — GET {{baseUrl}}/users/{{newUserId}}:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Created user is retrievable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newUserId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the collection (the Runner executes top to bottom) and the two requests are now one test: &lt;em&gt;creation actually persisted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why &lt;code&gt;pm.collectionVariables&lt;/code&gt; and not &lt;code&gt;pm.environment&lt;/code&gt;? The captured ID is run-state, not configuration — it belongs to the suite, not to "staging vs production". Keeping run-state in collection scope also means the chain works no matter which environment is selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full lifecycle chain
&lt;/h2&gt;

&lt;p&gt;The canonical workflow folder — five requests, each verifying the last:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;📁 User lifecycle
   1. POST   /users            → 201, capture newUserId
   2. GET    /users/{{newUserId}} → 200, fields match what was sent
   3. PATCH  /users/{{newUserId}} → 200, capture nothing, send {"name": "Updated"}
   4. GET    /users/{{newUserId}} → 200, name is "Updated", other fields unchanged
   5. DELETE /users/{{newUserId}} → 204
   6. GET    /users/{{newUserId}} → 404  ← the test that actually proves deletion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4 deserves its subtle assertion: not just that the patched field changed, but that the &lt;em&gt;others didn't&lt;/em&gt; — PATCH endpoints that quietly reset unrelated fields are a classic, expensive bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PATCH changed only the name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Updated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;originalEmail&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Capture &lt;code&gt;originalEmail&lt;/code&gt; back in step 1, same one-liner pattern.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Fresh data every run
&lt;/h2&gt;

&lt;p&gt;A chain that creates &lt;code&gt;test@example.com&lt;/code&gt; works exactly once against a persistent database — the second run collides with the first run's leftovers. Postman's dynamic variables fix this with zero code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{$randomFullName}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{$randomEmail}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{$guid}}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every run creates unique records. And the companion discipline: &lt;strong&gt;tests that create data should delete it&lt;/strong&gt; — which the lifecycle chain does by design, steps 5–6. A suite that cleans up after itself is a suite you can run any time, from anywhere, including every 15 minutes from CI. Re-runnability is the cardinal virtue of automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controlling the flow
&lt;/h2&gt;

&lt;p&gt;By default the Runner goes top to bottom. Scripts can redirect it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Skip to teardown if creation failed — no point testing a ghost&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNextRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cleanup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Or end the run entirely&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNextRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;postman.setNextRequest&lt;/code&gt; is the legacy name; &lt;code&gt;pm.execution.setNextRequest&lt;/code&gt; is current.) Use it sparingly — a suite that jumps around is hard to read — but skip-on-failure and polling loops are exactly what it's for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep chains short
&lt;/h2&gt;

&lt;p&gt;One warning from maintenance experience: chains are powerful and chains are coupling. A folder where request 6 depends on request 1 is fine; a collection where request 40 depends on request 3 is unmaintainable. The discipline that scales: &lt;strong&gt;each workflow folder is self-contained&lt;/strong&gt; — it creates what it needs, verifies behaviour, and destroys what it made. Folders can run independently; requests within a folder cannot. That's the right amount of coupling.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adapted from Chapter 7 of my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;*&lt;em&gt;. Read online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postmanapi</category>
      <category>testing</category>
      <category>api</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Postman Variable Mistake That Leaks Tokens (and the 5-Scope Model That Prevents It)</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/the-postman-variable-mistake-that-leaks-tokens-and-the-5-scope-model-that-prevents-it-1fhp</link>
      <guid>https://dev.to/imranalmunyeem/the-postman-variable-mistake-that-leaks-tokens-and-the-5-scope-model-that-prevents-it-1fhp</guid>
      <description>&lt;p&gt;Every Postman environment variable has two value fields, and the difference between them is a security boundary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initial values sync to Postman's servers and are shared with everyone who can see the workspace. Current values stay on your machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The classic leak writes itself: someone pastes a Bearer token into the &lt;em&gt;initial&lt;/em&gt; value "just for a second" to test something. It syncs. A teammate forks the collection into another workspace. Someone makes a workspace public to share a demo. The token is now indexed, shared, or both — and nobody typed a single careless character; they just used the wrong column.&lt;/p&gt;

&lt;p&gt;Once you know the rule, prevention is trivial. So here's the rule, plus the rest of Postman's variable model — because the same five-scope system that keeps secrets safe is also what makes one collection run against staging, production, and CI without editing a single request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five scopes, narrowest wins
&lt;/h2&gt;

&lt;p&gt;Postman resolves &lt;code&gt;{{variables}}&lt;/code&gt; through five scopes, and the narrowest matching scope wins:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;local &amp;gt; data &amp;gt; environment &amp;gt; collection &amp;gt; global&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global&lt;/strong&gt; — exist outside any environment. Quick and dirty; fine for prototyping, wrong for anything permanent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collection&lt;/strong&gt; — travel &lt;em&gt;with&lt;/em&gt; the collection. Ideal for constants that belong to the suite itself (API version strings, fixed test-record IDs) — they work even when someone imports your collection without your environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment&lt;/strong&gt; — the workhorses. Everything that differs between deployments: base URLs, credentials, tenant IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt; — come from a CSV/JSON file during data-driven runs; each iteration reads one row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local&lt;/strong&gt; — set in scripts, live for a single request or iteration, then vanish. The override-everything scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In scripts, each has an API — &lt;code&gt;pm.environment.get("baseUrl")&lt;/code&gt;, &lt;code&gt;pm.collectionVariables.set("userId", id)&lt;/code&gt;, &lt;code&gt;pm.globals.get(...)&lt;/code&gt; — plus &lt;code&gt;pm.variables.get(...)&lt;/code&gt;, which walks the precedence chain for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that makes suites portable
&lt;/h2&gt;

&lt;p&gt;One discipline turns this from trivia into leverage: &lt;strong&gt;environments should differ in values, never in structure.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;Staging&lt;/code&gt; and &lt;code&gt;Production&lt;/code&gt; with &lt;em&gt;identical variable names&lt;/em&gt; — &lt;code&gt;baseUrl&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;tenantId&lt;/code&gt; — and different values. Every request references &lt;code&gt;{{baseUrl}}&lt;/code&gt;; no request knows which environment exists. Switching your entire suite between deployments becomes a one-click act, and pointing CI at a third environment is a file, not a refactor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Requests use:   GET {{baseUrl}}/users/{{userId}}&lt;/span&gt;
&lt;span class="c1"&gt;// Scripts use:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baseUrl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you ever find yourself editing a request to switch environments, a variable is missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling actual secrets
&lt;/h2&gt;

&lt;p&gt;Four rules, in increasing order of paranoia:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Secrets go in current values only.&lt;/strong&gt; Never initial. The initial value of &lt;code&gt;token&lt;/code&gt; can be empty or a placeholder like &lt;code&gt;SET-ME-LOCALLY&lt;/code&gt; — that placeholder syncing to teammates is a feature, because it documents what they need to supply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Set the variable type to &lt;code&gt;secret&lt;/code&gt;.&lt;/strong&gt; It masks the value on screen — protection against shoulder-surfing, screen shares, and screenshots in bug reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. For the truly sensitive, use Postman Vault.&lt;/strong&gt; Vault values are encrypted locally and never sync at all — referenced as &lt;code&gt;{{vault:my-token}}&lt;/code&gt;. The right home for production credentials, if they must exist in Postman at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. In CI, no files carry secrets.&lt;/strong&gt; Exported environment JSON contains values in plain text, so exported environments must be sanitised before committing. The real credential enters at runtime from the pipeline's secret store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;newman run collection.json &lt;span class="nt"&gt;-e&lt;/span&gt; Staging.postman_environment.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--env-var&lt;/span&gt; &lt;span class="s2"&gt;"token=&lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--env-var&lt;/code&gt; injects it as an environment-scoped variable for that run only — nothing on disk, nothing in the repo, masked in CI logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two debugging tips that save an hour each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Orange vs red.&lt;/strong&gt; A resolved &lt;code&gt;{{variable}}&lt;/code&gt; renders orange in the URL bar; an unresolved one renders red. Red means a typo in the name — or, nine times out of ten, &lt;em&gt;no environment selected&lt;/em&gt; in the dropdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Console never lies.&lt;/strong&gt; Postman's Console (in the footer) shows every request &lt;em&gt;as actually sent&lt;/em&gt; — variables resolved, final headers. When a request "mysteriously" hits the wrong host or sends &lt;code&gt;{{token}}&lt;/code&gt; literally, the Console ends the mystery in seconds. &lt;code&gt;console.log()&lt;/code&gt; from your scripts lands there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;Same names across environments, secrets in current values (or Vault), &lt;code&gt;--env-var&lt;/code&gt; in CI — and the entire class of "it works on my machine / we leaked a token" problems disappears from your Postman practice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adapted from Chapter 6 of my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;*&lt;em&gt;. Read online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postmanapi</category>
      <category>security</category>
      <category>testing</category>
      <category>api</category>
    </item>
    <item>
      <title>From Postman Collection to CI Gate: The Complete GitHub Actions Pipeline</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/from-postman-collection-to-ci-gate-the-complete-github-actions-pipeline-3p0d</link>
      <guid>https://dev.to/imranalmunyeem/from-postman-collection-to-ci-gate-the-complete-github-actions-pipeline-3p0d</guid>
      <description>&lt;p&gt;A Postman collection that runs when you click Run is a tool. The same collection running on every push — turning the build red before a broken API reaches anyone — is infrastructure.&lt;/p&gt;

&lt;p&gt;The gap between those two states is smaller than most teams think: one exported file, one secret, and about 30 lines of YAML. Here's the complete pipeline, including the parts tutorials usually skip (secrets, reports, and what actually makes the build fail).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Put the collection in the repo
&lt;/h2&gt;

&lt;p&gt;Export your collection (&lt;strong&gt;[…] → Export → Collection v2.1&lt;/strong&gt;) and any environment, and commit them beside the code they test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-repo/
├── src/
├── collections/
│   └── MyCollection.postman_collection.json
├── environments/
│   └── Staging.postman_environment.json
└── .github/workflows/api-tests.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the pattern that scales: tests are versioned, reviewed in PRs, and branch with features. (One compatibility note: export &lt;strong&gt;v2.1&lt;/strong&gt;, not the newer v3 YAML format — Newman can't run v3; only the official Postman CLI can.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strip secrets from the environment file first.&lt;/strong&gt; Exported environments contain values in plain text. Tokens don't go in the file — they go in the pipeline's secret store, injected at runtime. That's step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — The workflow
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/api-tests.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API Tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;   &lt;span class="c1"&gt;# nightly at 02:00 UTC&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postman-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lts/*"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Newman&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g newman newman-reporter-htmlextra&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run collection&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.API_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;newman run collections/MyCollection.postman_collection.json&lt;/span&gt;
          &lt;span class="s"&gt;-e environments/Staging.postman_environment.json&lt;/span&gt;
          &lt;span class="s"&gt;--env-var "token=$API_TOKEN"&lt;/span&gt;
          &lt;span class="s"&gt;-r cli,htmlextra&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;newman-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;newman/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each piece buys you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;on: push&lt;/code&gt; + &lt;code&gt;schedule&lt;/code&gt;&lt;/strong&gt; — every code change is tested, and the nightly run catches drift in the API even on quiet days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--env-var "token=$API_TOKEN"&lt;/code&gt;&lt;/strong&gt; — the credential enters at runtime from GitHub's encrypted secrets, never touching a file or the logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-r cli,htmlextra&lt;/code&gt;&lt;/strong&gt; — console output for the Actions log &lt;em&gt;and&lt;/em&gt; a rich HTML report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;if: always()&lt;/code&gt;&lt;/strong&gt; on the upload — you want the report &lt;em&gt;especially&lt;/em&gt; when tests fail; without &lt;code&gt;always()&lt;/code&gt;, a red run skips the step and eats your evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3 — The secret
&lt;/h2&gt;

&lt;p&gt;Repo → &lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt; → name it &lt;code&gt;API_TOKEN&lt;/code&gt;, paste the value. Reference it only via &lt;code&gt;${{ secrets.API_TOKEN }}&lt;/code&gt; as above. GitHub masks it in logs automatically.&lt;/p&gt;

&lt;p&gt;In the collection, the request reads it as &lt;code&gt;{{token}}&lt;/code&gt; — same as any Postman variable. Locally you keep the real value in the environment's &lt;em&gt;current value&lt;/em&gt; (never synced, never exported); in CI it arrives via &lt;code&gt;--env-var&lt;/code&gt;. One collection, two credential sources, zero leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the build actually fails
&lt;/h2&gt;

&lt;p&gt;No configuration needed: &lt;strong&gt;Newman exits non-zero when any test fails&lt;/strong&gt;, and a non-zero exit code fails the Actions step, which fails the workflow, which blocks the merge if you've made the check required (&lt;strong&gt;Settings → Branch protection → require status checks&lt;/strong&gt;). That exit code is the entire contract between your tests and your pipeline — the same mechanism works identically in Jenkins, GitLab CI, CircleCI, and Azure Pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Postman CLI variant
&lt;/h2&gt;

&lt;p&gt;Prefer running the live collection from your workspace instead of an exported file? Swap two steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Postman CLI&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;curl -o- "https://dl-cli.pstmn.io/install/linux64.sh" | sh&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run collection&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTMAN_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.POSTMAN_API_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;postman login --with-api-key "$POSTMAN_API_KEY"&lt;/span&gt;
          &lt;span class="s"&gt;postman collection run YOUR-COLLECTION-ID -e YOUR-ENVIRONMENT-ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exports to keep in sync, and each run posts a shareable report back into your Postman workspace. The trade-off: your CI now depends on Postman's cloud being reachable, and the "tests as code, reviewed in PRs" property weakens. Teams that treat the collection as source code tend to stay with exported files; teams that live in Postman's workspace tend to prefer the CLI. Both are legitimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export collection (v2.1) + sanitised environment into the repo&lt;/li&gt;
&lt;li&gt;Add the workflow file&lt;/li&gt;
&lt;li&gt;Add the secret; wire it with &lt;code&gt;--env-var&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Make the check required in branch protection&lt;/li&gt;
&lt;li&gt;Open a PR with a deliberately failing test and watch it get blocked — &lt;em&gt;that's&lt;/em&gt; your proof the gate works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time, honestly: under an hour. Ongoing servers to maintain: zero.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adapted from Chapter 13 of my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;** — which also covers the full Jenkins setup if that's your shop. Read online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>testing</category>
      <category>postmanapi</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Contract Testing in 10 Lines: JSON Schema Validation in Postman</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 10 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/contract-testing-in-10-lines-json-schema-validation-in-postman-27ih</link>
      <guid>https://dev.to/imranalmunyeem/contract-testing-in-10-lines-json-schema-validation-in-postman-27ih</guid>
      <description>&lt;p&gt;Here's a bug your test suite probably wouldn't catch.&lt;/p&gt;

&lt;p&gt;A backend developer refactors the user model. The &lt;code&gt;id&lt;/code&gt; field — an integer since forever — starts coming back as a string: &lt;code&gt;"42"&lt;/code&gt; instead of &lt;code&gt;42&lt;/code&gt;. Every value is still "correct". Your assertion &lt;code&gt;pm.expect(user.id).to.eql(42)&lt;/code&gt; fails, sure — but only on the one endpoint you asserted &lt;code&gt;id&lt;/code&gt; on, not the other nine that return users. Meanwhile three client apps that did &lt;code&gt;user.id + 1&lt;/code&gt; are now computing &lt;code&gt;"421"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;structural drift&lt;/strong&gt;, and it's what actually breaks API consumers: renamed fields, changed types, properties that quietly vanish. Field-by-field value assertions catch it patchily and by accident. &lt;strong&gt;Schema validation catches it systematically&lt;/strong&gt; — and in Postman it costs about ten lines, because the ajv JSON-schema validator is built into the script sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten lines
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Scripts → Post-response&lt;/strong&gt; on any request that returns a user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response matches the user schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single test now fails if &lt;code&gt;id&lt;/code&gt; becomes a string, if &lt;code&gt;email&lt;/code&gt; disappears, if &lt;code&gt;name&lt;/code&gt; becomes an object — every structural mutation, whether or not you thought to assert on that field's value.&lt;/p&gt;

&lt;p&gt;For an endpoint returning an array of users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userListSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;minItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt;   &lt;span class="c1"&gt;// reuse the object schema&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List matches schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userListSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Share one schema across every endpoint
&lt;/h2&gt;

&lt;p&gt;The real power move: your API returns users from &lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/users/:id&lt;/code&gt;, &lt;code&gt;/login&lt;/code&gt;, &lt;code&gt;/teams/:id/members&lt;/code&gt;… and they should all be &lt;em&gt;the same shape&lt;/em&gt;. Store the schema once as a collection variable (JSON, stringified), and every request validates against the same contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One-time setup — e.g. in the collection's Pre-request script:&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userSchema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In any request's Post-response script:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userSchema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User matches the shared contract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the contract legitimately changes — a new required field, say — you update one variable and every endpoint's test updates with it. That's contract testing: not a new tool, just the discipline of asserting &lt;em&gt;shape&lt;/em&gt; centrally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema tips that save real debugging time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;required&lt;/code&gt; is where the protection lives.&lt;/strong&gt; JSON Schema ignores missing properties unless they're listed in &lt;code&gt;required&lt;/code&gt;. A schema without &lt;code&gt;required&lt;/code&gt; validates &lt;code&gt;{}&lt;/code&gt; happily — the most common reason people think their schema test "isn't working".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type-check numbers deliberately.&lt;/strong&gt; &lt;code&gt;integer&lt;/code&gt; vs &lt;code&gt;number&lt;/code&gt; matters — prices and quantities have different rules. And remember the drift that motivates all this is usually number→string, so never write &lt;code&gt;type: ["integer", "string"]&lt;/code&gt; to make a flaky test pass. That's deleting the alarm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Formats catch bad data early.&lt;/strong&gt; &lt;code&gt;pattern&lt;/code&gt; for emails and IDs, &lt;code&gt;enum&lt;/code&gt; for status fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suspended&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deleted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone adds a fourth status without telling anyone, you find out from a red test instead of a production incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start schemas from reality, not from scratch.&lt;/strong&gt; Send the request once, copy the response, and derive the schema from it (an LLM does this conversion well — &lt;em&gt;then review it against the spec&lt;/em&gt;, especially which fields are truly &lt;code&gt;required&lt;/code&gt;; a schema generated from one happy response will mark optional fields as required and miss fields that were null that day).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits in a strategy
&lt;/h2&gt;

&lt;p&gt;Schema validation is one layer of a complete endpoint checklist — protocol (status, headers, timing), &lt;strong&gt;contract (this)&lt;/strong&gt;, data values, behaviour (state actually changed), and security (auth negatives). It's the layer with the best effort-to-protection ratio in the whole list: ten lines per shape, and an entire category of consumer-breaking regressions becomes impossible to ship silently.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adapted from Chapter 7 of my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;*&lt;em&gt;. Read online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>postmanapi</category>
      <category>json</category>
      <category>qa</category>
    </item>
    <item>
      <title>Still Testing APIs Like It's 2022? Here's Everything That Changed"</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 03 Aug 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/still-testing-apis-like-its-2022-heres-everything-that-changed-28l8</link>
      <guid>https://dev.to/imranalmunyeem/still-testing-apis-like-its-2022-heres-everything-that-changed-28l8</guid>
      <description>&lt;p&gt;If you learnt Postman a few years ago and have been coasting on that knowledge, an uncomfortable amount of it is now wrong — not "slightly dated" wrong, but "the tab you're looking for doesn't exist" wrong.&lt;/p&gt;

&lt;p&gt;I know because I wrote a Postman guide in 2022 and recently rewrote it as a full book. Here's the changelog of everything that broke, moved, or appeared — the field guide I wish I'd had.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Tests tab is gone
&lt;/h2&gt;

&lt;p&gt;The old &lt;strong&gt;Tests&lt;/strong&gt; and &lt;strong&gt;Pre-request Script&lt;/strong&gt; tabs were merged into a single &lt;strong&gt;Scripts&lt;/strong&gt; tab with two sections: &lt;strong&gt;Pre-request&lt;/strong&gt; (runs before the request) and &lt;strong&gt;Post-response&lt;/strong&gt; (runs after — this is where your tests live now).&lt;/p&gt;

&lt;p&gt;Your code is unchanged — same &lt;code&gt;pm.test&lt;/code&gt;, same Chai assertions, same snippets — only the geography moved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Exactly the same as it ever was, just under Scripts → Post-response&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status code is 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tutorial screenshot showing a Tests tab is from the old world. The concepts transfer completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Newman has an official successor
&lt;/h2&gt;

&lt;p&gt;Newman — the beloved open-source CLI runner — is no longer the only way to run collections from a terminal. The official &lt;strong&gt;Postman CLI&lt;/strong&gt; authenticates with a Postman API key and can run collections &lt;strong&gt;straight from your workspace, no export step&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postman login &lt;span class="nt"&gt;--with-api-key&lt;/span&gt; &lt;span class="nv"&gt;$POSTMAN_API_KEY&lt;/span&gt;
postman collection run 12345678-abcd-efgh-ijkl-9876543210ab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also pushes results back to Postman's cloud as a shareable run report.&lt;/p&gt;

&lt;p&gt;Newman still works fine and its htmlextra HTML reports remain unmatched — but there's a hard compatibility line to know: &lt;strong&gt;Newman only supports collection format v2/v2.1&lt;/strong&gt;. Postman v12 introduced a v3 (YAML) format for its Git-native workflows, and Newman cannot run it. New pipeline? Start with the Postman CLI. Existing Newman pipeline? It keeps working — just export v2.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your Jenkins tutorial will not work
&lt;/h2&gt;

&lt;p&gt;The classic "install JDK 8, run &lt;code&gt;java -jar jenkins.war&lt;/code&gt;" instructions are dead: &lt;strong&gt;modern Jenkins requires Java 21&lt;/strong&gt; (recent LTS lines dropped Java 17 and older). If you're standing up a CI box today, grab Temurin 21 from adoptium.net first, or Jenkins simply won't start.&lt;/p&gt;

&lt;p&gt;Better yet, notice that for many teams the Jenkins box is now optional — a 25-line GitHub Actions workflow runs your collection on every push and every night, with no server to patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API Tests&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postman-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lts/*"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g newman newman-reporter-htmlextra&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;newman run collections/MyCollection.postman_collection.json -r cli,htmlextra&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. There's an AI in the footer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Postbot&lt;/strong&gt; generates tests from plain-English prompts, adds baseline tests to a whole collection in one action, repairs failing scripts, documents requests, and visualises responses. It's genuinely useful and genuinely dangerous in the same specific way: it writes assertions from the response your API &lt;em&gt;currently gives&lt;/em&gt; — so if the current behaviour is a bug, the bug becomes the expected result. Generate freely; review against the spec, not the response. (I wrote a whole piece on this failure mode.)&lt;/p&gt;

&lt;p&gt;Postman also added AI request types (test your LLM-backed endpoints like any other), Agent Mode, and an MCP server — the platform is clearly betting that agents will be first-class API consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Your practice APIs died
&lt;/h2&gt;

&lt;p&gt;Heroku ended its free tier, taking a generation of tutorial APIs with it. If a guide points you at &lt;code&gt;something.herokuapp.com&lt;/code&gt;, expect a dead link. &lt;strong&gt;JSONPlaceholder&lt;/strong&gt; (&lt;code&gt;jsonplaceholder.typicode.com&lt;/code&gt;) remains free, signup-less, and reliable — or create a mock server inside Postman itself from your saved examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The scripting API moved on politely
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;postman.setNextRequest(null)&lt;/code&gt; still works, but the current form is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNextRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// stop the run&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNextRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Delete user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// or jump to a request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story across the &lt;code&gt;pm&lt;/code&gt; API: old code keeps running, new code should use the new names.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Secrets got first-class treatment
&lt;/h2&gt;

&lt;p&gt;Two things every team should know: variables now have a &lt;strong&gt;secret type&lt;/strong&gt; (masked on screen), and — the one that catches people — &lt;strong&gt;initial values sync to Postman's servers and are shared with collaborators, while current values stay on your machine.&lt;/strong&gt; Real credentials go in current values only, or in Postman Vault (encrypted, local, never synced). The classic leak is a Bearer token pasted into an initial value "just for a second," then synced, then forked into a public workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;None of this changed what good API testing &lt;em&gt;is&lt;/em&gt; — five layers of assertions, negative cases, deterministic suites, CI enforcement. What changed is the tooling around it, and the tooling changed enough that 2022 muscle memory now produces broken pipelines and missing tabs. Update the muscle memory; keep the principles.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the story behind my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;** — a 2022 guide rewritten end-to-end for how we test now. Read online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>postmanapi</category>
      <category>qa</category>
    </item>
    <item>
      <title>AI Writes Your API Tests Now. Here's the Bug It Introduces.</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/ai-writes-your-api-tests-now-heres-the-bug-it-introduces-3kgf</link>
      <guid>https://dev.to/imranalmunyeem/ai-writes-your-api-tests-now-heres-the-bug-it-introduces-3kgf</guid>
      <description>&lt;p&gt;Ask an AI to write tests for your API endpoint and it will do something subtly dangerous: it will look at the response your API &lt;em&gt;currently gives&lt;/em&gt; and write assertions that lock it in.&lt;/p&gt;

&lt;p&gt;If that response is correct — great, you just saved twenty minutes. If that response is a bug — say, your API wrongly returns &lt;code&gt;200 OK&lt;/code&gt; for an invalid payload — the AI will happily generate &lt;code&gt;pm.test("Status code is 200", ...)&lt;/code&gt; and your bug is now &lt;em&gt;the expected behaviour&lt;/em&gt;, defended by a green test forever.&lt;/p&gt;

&lt;p&gt;That's the whole tension of AI-assisted testing in one sentence: &lt;strong&gt;AI-generated tests validate what a response is, not what it should be.&lt;/strong&gt; The specification lives in your head and your docs; the AI only sees the wire.&lt;/p&gt;

&lt;p&gt;I use AI test generation daily and I'm not telling you to avoid it — I'm telling you how to use it without it quietly rotting your suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI is genuinely great at
&lt;/h2&gt;

&lt;p&gt;Postman's built-in assistant, Postbot, is the one most API testers will meet first (the pattern applies equally to Copilot, Claude, or whatever you use). Its strongest moves:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boilerplate generation.&lt;/strong&gt; Open a request, send it once so a response exists, open &lt;strong&gt;Scripts → Post-response&lt;/strong&gt;, and prompt in plain English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Add tests for the status code, the content type, and that the response time is under 500 ms."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It writes idiomatic &lt;code&gt;pm.test&lt;/code&gt; / &lt;code&gt;pm.expect&lt;/code&gt; code straight into the editor. For an entire collection, &lt;strong&gt;Generate tests&lt;/strong&gt; from the collection menu adds baseline tests to every request at once — hours of boilerplate become minutes of review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixing broken scripts.&lt;/strong&gt; When a test throws an error, Postbot's &lt;strong&gt;Fix script&lt;/strong&gt; analyses it and proposes a correction. For syntax slips and stale JSON paths, this is close to magic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exploration.&lt;/strong&gt; "What's wrong with this request?", "visualise this response as a table", "document this collection" — context-aware answers that used to mean twenty minutes of reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test-case brainstorming.&lt;/strong&gt; Outside Postman, feeding an OpenAPI spec to an LLM and asking for edge cases is genuinely productive: it will suggest boundary values, weird Unicode, and state transitions you hadn't listed. Treat the output as a &lt;em&gt;checklist to evaluate&lt;/em&gt;, not a suite to paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fails, specifically
&lt;/h2&gt;

&lt;p&gt;Three failure modes recur:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The observed-behaviour trap&lt;/strong&gt; (the big one, above). Every generated assertion needs review against the &lt;em&gt;spec&lt;/em&gt;, not the response. The question is never "does this match what came back?" — the AI already guaranteed that. The question is "is what came back correct?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Happy-path bias.&lt;/strong&gt; Ask for tests and you'll get success cases. The negative suite — wrong passwords, missing fields, expired tokens, malformed bodies, the requests an attacker sends — still needs a human's adversarial instinct. My rule of thumb: a real endpoint needs five to ten negative/boundary cases per positive case, and the AI will volunteer roughly none of them unprompted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Plausible nonsense.&lt;/strong&gt; Occasionally the generated assertion is syntactically perfect and semantically empty — checking that an array "exists" rather than that it contains the right things. A suite full of these is worse than a small honest one, because it &lt;em&gt;looks&lt;/em&gt; like coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The working protocol
&lt;/h2&gt;

&lt;p&gt;What this adds up to in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Learn to write tests by hand first.&lt;/strong&gt; Not for purity — because review is the whole job now, and you cannot review code you couldn't have written. A test you can't read is a test you can't trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate, then interrogate.&lt;/strong&gt; For each AI assertion ask: does the spec say this? If the response were wrong in the most likely way, would this catch it?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the negatives yourself.&lt;/strong&gt; Budget the time you saved on boilerplate for the adversarial cases the AI skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never paste secrets into prompts.&lt;/strong&gt; Tokens, customer data, internal URLs — they don't belong in any AI conversation unless your plan contractually protects them. (Postbot on enterprise plans excludes inputs from training; check your tier's terms.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the suite.&lt;/strong&gt; If a generated test fails six months from now, someone has to understand it in ninety seconds. That someone is you.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The honest framing
&lt;/h2&gt;

&lt;p&gt;Used this way, AI does for test &lt;em&gt;authoring&lt;/em&gt; what Newman did for test &lt;em&gt;execution&lt;/em&gt;: it removes the drudgery and leaves you the judgement. The testers who struggle with AI are the ones who hand over the judgement too — and then wonder why their green suite missed the bug that was green by design.&lt;/p&gt;

&lt;p&gt;Machines for repetition, humans for meaning. That division of labour was true before LLMs and it will be true after whatever comes next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adapted from Chapter 14 of my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;** — 15 chapters from your first request to AI-assisted CI/CD pipelines. Read online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>postmanapi</category>
      <category>qa</category>
    </item>
    <item>
      <title>How to Design an API Test Strategy (Not Just Accumulate Tests)</title>
      <dc:creator>Imran Al Munyeem</dc:creator>
      <pubDate>Mon, 20 Jul 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/imranalmunyeem/how-to-design-an-api-test-strategy-not-just-accumulate-tests-2bmp</link>
      <guid>https://dev.to/imranalmunyeem/how-to-design-an-api-test-strategy-not-just-accumulate-tests-2bmp</guid>
      <description>&lt;p&gt;Most API test suites are decorative.&lt;/p&gt;

&lt;p&gt;They're green, they run on every build, and they check almost nothing that matters: a wall of &lt;code&gt;status is 200&lt;/code&gt; assertions over happy-path requests. When the API starts returning the wrong data, the wrong errors, or leaking internals in a stack trace, the suite stays green — because nobody ever decided what it was supposed to catch.&lt;/p&gt;

&lt;p&gt;Tools execute tests; testers &lt;em&gt;choose&lt;/em&gt; them. Given an endpoint, what should you actually verify? Here's the framework I use — five layers, three axes of case design, and a way to prioritise when you can't test everything first. Examples use Postman's test scripts, but the framework is tool-agnostic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layered checklist
&lt;/h2&gt;

&lt;p&gt;For every endpoint that matters, work outside-in through five layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Protocol layer
&lt;/h3&gt;

&lt;p&gt;The right status code for the right situation — and &lt;em&gt;specific&lt;/em&gt; failures, not generic ones: 400 for malformed input, 401 for missing credentials, 403 for insufficient rights, 404 for absent resources. Plus the correct &lt;code&gt;Content-Type&lt;/code&gt;, and a sensible response time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status is 200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type is JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Responds within 500 ms&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseTime&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;below&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap to write, catches an entire class of regressions. But if your suite stops here, it's decorative.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Contract layer
&lt;/h3&gt;

&lt;p&gt;The response &lt;em&gt;structure&lt;/em&gt; matches its schema: required fields present, types correct, formats valid. This is what protects the API's consumers — a renamed field or a number that quietly became a string breaks every client, while your value assertions may not even notice.&lt;/p&gt;

&lt;p&gt;Postman ships the ajv JSON-schema validator, so contract testing is ten lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Response matches the user schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jsonSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Data layer
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;values&lt;/em&gt; are right: the created user has the name you sent; the filtered list contains only matching records; the total equals the sum of the parts; page 2 is actually different from page 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Filter returns only active users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Behaviour layer
&lt;/h3&gt;

&lt;p&gt;State actually changed. After DELETE, a follow-up GET returns 404 — deletion that only &lt;em&gt;claims&lt;/em&gt; to work is a classic bug. After PATCH, only the patched fields differ. POSTing the same order twice doesn't charge twice.&lt;/p&gt;

&lt;p&gt;These require chaining requests — capture an ID from one response and use it in the next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the POST's post-response script:&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collectionVariables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newUserId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Next request: GET {{baseUrl}}/users/{{newUserId}}&lt;/span&gt;
&lt;span class="c1"&gt;// Then: DELETE, then GET again expecting 404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behaviour layer is where the most expensive bugs live, and it's the layer happy-path suites never reach.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Security layer
&lt;/h3&gt;

&lt;p&gt;Every negative auth case: no token (expect 401), an expired token (401), a valid token for the &lt;em&gt;wrong&lt;/em&gt; user (403), a valid token with insufficient role (403). Plus: error responses that don't leak stack traces or internal hostnames.&lt;/p&gt;

&lt;p&gt;Authentication isn't plumbing to get through on the way to your tests. It &lt;em&gt;is&lt;/em&gt; a test surface — usually the highest-risk one you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Positive, negative, and boundary
&lt;/h2&gt;

&lt;p&gt;For each input an endpoint accepts, generate cases along three axes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Positive&lt;/strong&gt; — valid, typical input succeeds. The case everyone writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Negative&lt;/strong&gt; — invalid input fails &lt;em&gt;correctly&lt;/em&gt;: missing required fields, wrong types, malformed JSON, unknown IDs, illegal state transitions. The assertion is not merely "it fails" but "it fails with the right status and a useful error body." An API that returns 500 for bad input has a bug even though it rejected the request — the server failed to validate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boundary&lt;/strong&gt; — the edges: empty strings, zero, negative numbers, maximum lengths, page sizes of 0 and 1 and the documented limit and the limit plus one, Unicode in text fields.&lt;/p&gt;

&lt;p&gt;A practical heuristic: for a typical endpoint, expect &lt;strong&gt;one or two positive cases and five to ten negative and boundary cases&lt;/strong&gt;. If your suite is mostly green-path, it is mostly decorative.&lt;/p&gt;

&lt;p&gt;In Postman, the clean way to industrialise this is a data file. One request, one parameterised test, a CSV of cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;expectedStatus&lt;/span&gt;
&lt;span class="k"&gt;valid&lt;/span&gt;&lt;span class="kp"&gt;@example&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;correct&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;200&lt;/span&gt;
&lt;span class="k"&gt;valid&lt;/span&gt;&lt;span class="kp"&gt;@example&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;wrong&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;401&lt;/span&gt;
&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;correct&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;400&lt;/span&gt;
&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;an&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;correct&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="k"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status matches the data file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;have&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iterationData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expectedStatus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a case is now a spreadsheet edit — which means anyone on the team can extend coverage, not just the people who write JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prioritising: you cannot test everything first
&lt;/h2&gt;

&lt;p&gt;Order the work by risk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Endpoints whose failure costs most&lt;/strong&gt; — auth, payment, anything that writes data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoints that change most often&lt;/strong&gt; — change is where regressions come from&lt;/li&gt;
&lt;li&gt;Everything else&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A ten-test suite on the login endpoint beats a hundred tests on a static reference lookup.&lt;/p&gt;

&lt;p&gt;And adopt the single highest-value habit in test automation — the &lt;strong&gt;regression contract&lt;/strong&gt;: every bug found in production earns a permanent test reproducing it. Over a year, this builds the most valuable suite you'll ever own, because it is provably aligned with how your system actually fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the suite honest
&lt;/h2&gt;

&lt;p&gt;Three disciplines keep a growing suite trustworthy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic tests.&lt;/strong&gt; A test that sometimes fails without a code change is worse than no test — it trains the team to ignore red. Hunt flakiness down; it's usually shared state, missing teardown, or time-dependent assertions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Independent tests where possible.&lt;/strong&gt; Chained workflows are necessary, but keep chains short and self-contained. A suite where request 40 depends on request 3 is unmaintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Readable failures.&lt;/strong&gt; Name tests so a red line states the defect. "PATCH ignores read-only fields" tells the developer everything before they even open the tool. "Test 12 failed" tells them nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line summary
&lt;/h2&gt;

&lt;p&gt;A test suite earns its keep not by the number of green ticks but by the &lt;em&gt;decisions&lt;/em&gt; behind them: five layers per endpoint, five-to-one negative-to-positive ratio, risk-ordered, deterministic, and readable when red.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is adapted from Chapter 8 of my free, open-source book *&lt;/em&gt;&lt;a href="https://imranalmunyeem.github.io/api-testing-using-postman/" rel="noopener noreferrer"&gt;API Testing Using Postman: The Practical Guide to Modern API Testing&lt;/a&gt;** — 15 chapters from your first request to AI-assisted CI/CD pipelines, with a 37-question interview FAQ. Read it online, grab the PDF/EPUB, or contribute on &lt;a href="https://github.com/imranalmunyeem/api-testing-using-postman" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.*&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a PhD researcher in Computer Science at Nottingham Trent University working on cybersecurity and AI-assisted security testing. More at &lt;a href="https://imranalmunyeem.com" rel="noopener noreferrer"&gt;imranalmunyeem.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>api</category>
      <category>qa</category>
      <category>postmanapi</category>
    </item>
  </channel>
</rss>
