<?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: Smeet Gohel</title>
    <description>The latest articles on DEV Community by Smeet Gohel (@smeetgohel).</description>
    <link>https://dev.to/smeetgohel</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%2F3988929%2F860dc4c8-8c5f-4ebb-8dbc-708dbc43de19.png</url>
      <title>DEV Community: Smeet Gohel</title>
      <link>https://dev.to/smeetgohel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smeetgohel"/>
    <language>en</language>
    <item>
      <title>Postman Collections vs OpenAPI Specs: Which Scales as a Source of Truth?</title>
      <dc:creator>Smeet Gohel</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:32:54 +0000</pubDate>
      <link>https://dev.to/smeetgohel/postman-collections-vs-openapi-specs-which-scales-as-a-source-of-truth-1eg4</link>
      <guid>https://dev.to/smeetgohel/postman-collections-vs-openapi-specs-which-scales-as-a-source-of-truth-1eg4</guid>
      <description>&lt;p&gt;&lt;em&gt;Our team kept both for two years. Eventually they drifted, and the postmortem from that drift is the reason this post exists.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first, maintaining both seemed perfectly reasonable.&lt;/p&gt;

&lt;p&gt;Our OpenAPI specification documented the API.&lt;/p&gt;

&lt;p&gt;Our Postman collection helped developers explore and test it.&lt;/p&gt;

&lt;p&gt;Each served a different purpose.&lt;/p&gt;

&lt;p&gt;Or so we thought.&lt;/p&gt;

&lt;p&gt;As the API grew, something subtle happened.&lt;/p&gt;

&lt;p&gt;A new endpoint would be added to the application.&lt;/p&gt;

&lt;p&gt;The OpenAPI specification would be updated during code review.&lt;/p&gt;

&lt;p&gt;The Postman collection would be updated a week later.&lt;/p&gt;

&lt;p&gt;Sometimes.&lt;/p&gt;

&lt;p&gt;Other times it wouldn't.&lt;/p&gt;

&lt;p&gt;Months later, developers started asking questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Which request body is correct?"&lt;/li&gt;
&lt;li&gt;"Why does the documentation say one thing but Postman sends something else?"&lt;/li&gt;
&lt;li&gt;"Why does the generated SDK accept a field that isn't in the collection?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually we realized we no longer had one source of truth.&lt;/p&gt;

&lt;p&gt;We had two.&lt;/p&gt;

&lt;p&gt;And neither one completely matched production.&lt;/p&gt;

&lt;p&gt;That experience forced us to rethink the role of each format.&lt;/p&gt;

&lt;p&gt;The conclusion wasn't that one was "better."&lt;/p&gt;

&lt;p&gt;It was that they solve different problems—and only one of them scales well as the authoritative API contract.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Each Format Actually Models (And What It Doesn't)
&lt;/h1&gt;

&lt;p&gt;The first mistake many teams make is assuming that Postman collections and OpenAPI specifications represent the same thing.&lt;/p&gt;

&lt;p&gt;They don't.&lt;/p&gt;

&lt;p&gt;Their goals are fundamentally different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Postman Collection Models
&lt;/h2&gt;

&lt;p&gt;A Postman collection describes interactions.&lt;/p&gt;

&lt;p&gt;It contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests&lt;/li&gt;
&lt;li&gt;Headers&lt;/li&gt;
&lt;li&gt;Variables&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Example payloads&lt;/li&gt;
&lt;li&gt;Test scripts&lt;/li&gt;
&lt;li&gt;Pre-request scripts&lt;/li&gt;
&lt;li&gt;Environment values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its focus is execution.&lt;/p&gt;

&lt;p&gt;Developers can immediately send requests and observe responses.&lt;/p&gt;

&lt;p&gt;That's incredibly valuable during development and debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Doesn't Model Well
&lt;/h2&gt;

&lt;p&gt;Collections don't naturally describe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete API contracts&lt;/li&gt;
&lt;li&gt;Reusable schemas&lt;/li&gt;
&lt;li&gt;Type relationships&lt;/li&gt;
&lt;li&gt;Polymorphism&lt;/li&gt;
&lt;li&gt;Code generation metadata&lt;/li&gt;
&lt;li&gt;Validation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While Postman has added schema-related capabilities over time, contract modeling isn't its primary design goal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an OpenAPI Specification Models
&lt;/h2&gt;

&lt;p&gt;OpenAPI focuses on describing the API itself.&lt;/p&gt;

&lt;p&gt;It defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Endpoints&lt;/li&gt;
&lt;li&gt;Operations&lt;/li&gt;
&lt;li&gt;Parameters&lt;/li&gt;
&lt;li&gt;Request bodies&lt;/li&gt;
&lt;li&gt;Response schemas&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Data models&lt;/li&gt;
&lt;li&gt;Error responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is structured around the contract.&lt;/p&gt;

&lt;p&gt;That contract can then power:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Client SDK generation&lt;/li&gt;
&lt;li&gt;Mock servers&lt;/li&gt;
&lt;li&gt;Contract validation&lt;/li&gt;
&lt;li&gt;Test generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenAPI isn't primarily an execution format.&lt;/p&gt;

&lt;p&gt;It's a specification format.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Drift Problem at the 50-Endpoint Mark
&lt;/h1&gt;

&lt;p&gt;For small APIs, maintaining both formats rarely feels painful.&lt;/p&gt;

&lt;p&gt;Imagine ten endpoints.&lt;/p&gt;

&lt;p&gt;Updating two files after each change is manageable.&lt;/p&gt;

&lt;p&gt;Now imagine fifty.&lt;/p&gt;

&lt;p&gt;Then one hundred.&lt;/p&gt;

&lt;p&gt;Eventually every API change requires updating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implementation&lt;/li&gt;
&lt;li&gt;OpenAPI specification&lt;/li&gt;
&lt;li&gt;Postman collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three artifacts.&lt;/p&gt;

&lt;p&gt;Three opportunities for drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Drift Begins
&lt;/h2&gt;

&lt;p&gt;A developer adds:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```text id="f5mvr0"&lt;br&gt;
PATCH /customers/{id}&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


The OpenAPI specification updates immediately.

The Postman collection still contains:



```text id="h1lgrg"
PUT /customers/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing breaks immediately.&lt;/p&gt;

&lt;p&gt;Weeks later:&lt;/p&gt;

&lt;p&gt;Someone imports the Postman collection.&lt;/p&gt;

&lt;p&gt;Someone else generates an SDK from OpenAPI.&lt;/p&gt;

&lt;p&gt;Now different teams are using different contracts.&lt;/p&gt;

&lt;p&gt;Both appear correct.&lt;/p&gt;

&lt;p&gt;Neither matches reality perfectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Gets Worse Over Time
&lt;/h2&gt;

&lt;p&gt;Drift compounds.&lt;/p&gt;

&lt;p&gt;Each inconsistent update creates another future inconsistency.&lt;/p&gt;

&lt;p&gt;Eventually developers stop trusting both artifacts.&lt;/p&gt;

&lt;p&gt;Instead they inspect the implementation directly.&lt;/p&gt;

&lt;p&gt;That's exactly what documentation should prevent.&lt;/p&gt;




&lt;h1&gt;
  
  
  Tooling Around Each: Code Generation, Documentation, Mocks, and Tests
&lt;/h1&gt;

&lt;p&gt;One of OpenAPI's biggest strengths is the surrounding ecosystem.&lt;/p&gt;

&lt;p&gt;A single specification can generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interactive documentation&lt;/li&gt;
&lt;li&gt;Type-safe SDKs&lt;/li&gt;
&lt;li&gt;Mock servers&lt;/li&gt;
&lt;li&gt;Validation middleware&lt;/li&gt;
&lt;li&gt;Contract tests&lt;/li&gt;
&lt;li&gt;API clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything originates from one contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Postman Tooling
&lt;/h2&gt;

&lt;p&gt;Postman excels in different areas.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manual exploration&lt;/li&gt;
&lt;li&gt;Team collaboration&lt;/li&gt;
&lt;li&gt;Environment management&lt;/li&gt;
&lt;li&gt;Request execution&lt;/li&gt;
&lt;li&gt;Automated collections&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's an outstanding productivity tool for developers and QA engineers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where OpenAPI Pulls Ahead
&lt;/h2&gt;

&lt;p&gt;When APIs become larger, the contract becomes increasingly valuable.&lt;/p&gt;

&lt;p&gt;Teams begin relying on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code generation&lt;/li&gt;
&lt;li&gt;Continuous validation&lt;/li&gt;
&lt;li&gt;Consumer-driven contracts&lt;/li&gt;
&lt;li&gt;API governance&lt;/li&gt;
&lt;li&gt;Version compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those workflows naturally align with the &lt;strong&gt;OpenAPI specification&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  They Complement Each Other
&lt;/h2&gt;

&lt;p&gt;This isn't an either/or situation.&lt;/p&gt;

&lt;p&gt;Postman helps humans interact with APIs.&lt;/p&gt;

&lt;p&gt;OpenAPI helps tools understand APIs.&lt;/p&gt;

&lt;p&gt;That's an important distinction.&lt;/p&gt;




&lt;h1&gt;
  
  
  Migrating Postman → OpenAPI Without Losing Examples
&lt;/h1&gt;

&lt;p&gt;One reason many teams hesitate to adopt OpenAPI is fear of losing years of curated request examples.&lt;/p&gt;

&lt;p&gt;Fortunately, that's usually unnecessary.&lt;/p&gt;

&lt;p&gt;A gradual migration works surprisingly well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Export Existing Collections
&lt;/h2&gt;

&lt;p&gt;Most collections already contain valuable examples.&lt;/p&gt;

&lt;p&gt;Those shouldn't disappear.&lt;/p&gt;

&lt;p&gt;Export them first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Generate a Baseline Specification
&lt;/h2&gt;

&lt;p&gt;Several tools can convert Postman collections into an initial OpenAPI document.&lt;/p&gt;

&lt;p&gt;The output usually requires cleanup, but it's an excellent starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Promote Examples
&lt;/h2&gt;

&lt;p&gt;Move useful request and response examples into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request schemas&lt;/li&gt;
&lt;li&gt;Response examples&lt;/li&gt;
&lt;li&gt;Components&lt;/li&gt;
&lt;li&gt;Reusable objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples become part of the specification itself rather than remaining hidden inside collections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Generate New Collections
&lt;/h2&gt;

&lt;p&gt;Once OpenAPI becomes authoritative, regenerate Postman collections whenever the specification changes.&lt;/p&gt;

&lt;p&gt;Instead of manually maintaining two sources, maintain one.&lt;/p&gt;

&lt;p&gt;Generate the other.&lt;/p&gt;

&lt;p&gt;This single workflow eliminates most synchronization problems.&lt;/p&gt;




&lt;h1&gt;
  
  
  When Keeping Both Actually Makes Sense
&lt;/h1&gt;

&lt;p&gt;Despite everything I've written so far, there is one situation where maintaining both is entirely reasonable.&lt;/p&gt;

&lt;p&gt;Consumer workflows.&lt;/p&gt;

&lt;p&gt;Imagine an external developer onboarding experience.&lt;/p&gt;

&lt;p&gt;OpenAPI provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Schemas&lt;/li&gt;
&lt;li&gt;SDK generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Postman provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ready-to-run requests&lt;/li&gt;
&lt;li&gt;Authentication helpers&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Example workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These serve different audiences.&lt;/p&gt;

&lt;p&gt;As long as the collection is generated from OpenAPI rather than maintained independently, both remain valuable.&lt;/p&gt;

&lt;p&gt;The key is understanding which artifact owns the contract.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Practical Recommendation
&lt;/h1&gt;

&lt;p&gt;If I were starting a new API project today, my workflow would look like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;p&gt;Design the API using OpenAPI.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;p&gt;Review the contract.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;p&gt;Generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;SDKs&lt;/li&gt;
&lt;li&gt;Mock servers&lt;/li&gt;
&lt;li&gt;Contract tests&lt;/li&gt;
&lt;li&gt;Postman collections&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;p&gt;Implement the API.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;p&gt;Validate implementation against the contract.&lt;/p&gt;

&lt;p&gt;Notice what's missing.&lt;/p&gt;

&lt;p&gt;Manual synchronization.&lt;/p&gt;

&lt;p&gt;Every artifact originates from the same specification.&lt;/p&gt;

&lt;p&gt;That's the real advantage.&lt;/p&gt;




&lt;h1&gt;
  
  
  Common Mistakes Teams Make
&lt;/h1&gt;

&lt;p&gt;Over time, I've seen several recurring patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treating Postman as Documentation
&lt;/h2&gt;

&lt;p&gt;Collections explain requests.&lt;/p&gt;

&lt;p&gt;They're not comprehensive API contracts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Maintaining Both Manually
&lt;/h2&gt;

&lt;p&gt;This almost always leads to drift.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ignoring Examples
&lt;/h2&gt;

&lt;p&gt;Examples are valuable.&lt;/p&gt;

&lt;p&gt;Move them into the specification.&lt;/p&gt;

&lt;p&gt;Don't lose them during migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Writing Tests Against Collections Alone
&lt;/h2&gt;

&lt;p&gt;Tests become stronger when they validate against the API contract rather than only request collections.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delaying OpenAPI Adoption
&lt;/h2&gt;

&lt;p&gt;The larger the API becomes, the harder migration becomes later.&lt;/p&gt;

&lt;p&gt;Starting early usually pays off.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The debate around &lt;strong&gt;Postman vs OpenAPI&lt;/strong&gt; often assumes they're competing technologies.&lt;/p&gt;

&lt;p&gt;I don't think that's the right way to view them.&lt;/p&gt;

&lt;p&gt;Postman is excellent for interacting with APIs.&lt;/p&gt;

&lt;p&gt;OpenAPI is excellent for describing APIs.&lt;/p&gt;

&lt;p&gt;Those are complementary goals.&lt;/p&gt;

&lt;p&gt;Where teams run into trouble is treating both as independent sources of truth.&lt;/p&gt;

&lt;p&gt;That approach works for small projects but becomes increasingly difficult as APIs evolve.&lt;/p&gt;

&lt;p&gt;Once an API reaches dozens of endpoints, keeping multiple manually maintained contracts synchronized becomes a maintenance problem rather than a productivity benefit.&lt;/p&gt;

&lt;p&gt;For most organizations, the sustainable approach is straightforward:&lt;/p&gt;

&lt;p&gt;Use OpenAPI as the authoritative contract.&lt;/p&gt;

&lt;p&gt;Generate documentation, SDKs, mocks, tests, and even Postman collections from that contract.&lt;/p&gt;

&lt;p&gt;Then let each tool do what it does best instead of asking every artifact to become the source of truth.&lt;/p&gt;

&lt;p&gt;If you're evaluating workflows or deciding how to structure your API tooling, &lt;strong&gt;the &lt;a href="https://totalshiftleft.ai/compare/totalshiftleft-vs-postman" rel="noopener noreferrer"&gt;Total Shift Left vs Postman breakdown&lt;/a&gt;&lt;/strong&gt; provides a practical comparison of how contract-first approaches differ from collection-first workflows and where each fits within a modern API development process.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Testing Async Jobs and Queues End-to-End (Without sleep())</title>
      <dc:creator>Smeet Gohel</dc:creator>
      <pubDate>Mon, 29 Jun 2026 13:51:54 +0000</pubDate>
      <link>https://dev.to/smeetgohel/testing-async-jobs-and-queues-end-to-end-without-sleep-4h7o</link>
      <guid>https://dev.to/smeetgohel/testing-async-jobs-and-queues-end-to-end-without-sleep-4h7o</guid>
      <description>&lt;p&gt;&lt;em&gt;Search the average backend test suite for &lt;code&gt;sleep&lt;/code&gt; or &lt;code&gt;wait_for&lt;/code&gt; and you'll find a depressing number of arbitrary numbers — 2, 5, sometimes 30.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those numbers usually have a story behind them.&lt;/p&gt;

&lt;p&gt;Someone wrote an asynchronous test that occasionally failed because a background job hadn't completed yet. To make it pass, they added a &lt;code&gt;sleep(2)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few months later, the infrastructure changed. The job occasionally took three seconds.&lt;/p&gt;

&lt;p&gt;The test became flaky again.&lt;/p&gt;

&lt;p&gt;Someone increased the timeout to five seconds.&lt;/p&gt;

&lt;p&gt;Later, another environment was slower, so the timeout became thirty seconds.&lt;/p&gt;

&lt;p&gt;Eventually, every asynchronous test in the suite was waiting far longer than necessary, pipelines became slower, and intermittent failures were dismissed as "just another flaky test."&lt;/p&gt;

&lt;p&gt;If you've experienced this cycle, you're not alone.&lt;/p&gt;

&lt;p&gt;Testing asynchronous systems is fundamentally different from testing synchronous APIs. The goal isn't to wait a fixed amount of time—it’s to detect &lt;strong&gt;when the expected outcome has actually happened&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Over the past few years, I've found that a few simple patterns eliminate most of the unnecessary sleeps while making asynchronous tests faster and far more reliable.&lt;/p&gt;

&lt;p&gt;Here's the approach.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Async APIs Are Hard to Test
&lt;/h1&gt;

&lt;p&gt;Unlike a synchronous REST endpoint, asynchronous workflows return before the real work has finished.&lt;/p&gt;

&lt;p&gt;A typical request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   │
POST /orders
   │
   ▼
API
   │
Stores message
   │
Returns 202 Accepted
   │
───────────────
Background Worker
   │
Processes message
   │
Updates database
   │
Publishes event
   │
Clears cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API responds immediately.&lt;/p&gt;

&lt;p&gt;The actual business logic happens later.&lt;/p&gt;

&lt;p&gt;The test now has two responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the request was accepted.&lt;/li&gt;
&lt;li&gt;Verify the background processing completed correctly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's where many teams reach for &lt;code&gt;sleep()&lt;/code&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why &lt;code&gt;sleep()&lt;/code&gt; Is Almost Always the Wrong Tool
&lt;/h1&gt;

&lt;p&gt;Consider this example:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&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;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&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="nx"&gt;orderId&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;order&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Completed&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;This works…&lt;/p&gt;

&lt;p&gt;until it doesn't.&lt;/p&gt;

&lt;p&gt;Problems include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The job finishes in 200 ms, but the test still waits five seconds.&lt;/li&gt;
&lt;li&gt;The job takes six seconds during peak load, and the test fails.&lt;/li&gt;
&lt;li&gt;CI machines are slower than local development.&lt;/li&gt;
&lt;li&gt;Multiple background jobs compete for resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixed delay becomes either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too short (flaky tests), or&lt;/li&gt;
&lt;li&gt;Too long (slow pipelines).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither outcome is desirable.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Polling Beats Sleep
&lt;/h1&gt;

&lt;p&gt;Instead of waiting for a fixed duration, wait for the condition you're expecting.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the order completed?

No.

Wait briefly.

Check again.

Still no.

Wait briefly.

Check again.

Yes.

Continue immediately.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test finishes as soon as the condition becomes true.&lt;/p&gt;

&lt;p&gt;Not one second later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Polling Without Hammering the Database
&lt;/h2&gt;

&lt;p&gt;One concern is excessive database traffic.&lt;/p&gt;

&lt;p&gt;Fortunately, polling doesn't require checking every millisecond.&lt;/p&gt;

&lt;p&gt;A practical strategy looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Poll every 250–500 ms.&lt;/li&gt;
&lt;li&gt;Stop immediately once the condition succeeds.&lt;/li&gt;
&lt;li&gt;Respect an overall timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;eventually&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;order&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Completed&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;Most jobs complete after only a few polling iterations.&lt;/p&gt;

&lt;p&gt;The database load remains minimal.&lt;/p&gt;




&lt;h1&gt;
  
  
  The &lt;code&gt;eventually()&lt;/code&gt; Helper — 20 Lines That Eliminate Most Sleeps
&lt;/h1&gt;

&lt;p&gt;One of the most useful utilities we've adopted is a simple helper called &lt;code&gt;eventually()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It repeatedly executes an assertion until either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It succeeds, or&lt;/li&gt;
&lt;li&gt;The timeout expires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified implementation looks like:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;eventually&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;)&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assertion&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interval&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Condition not satisfied before timeout.&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;Despite being only a few lines of code, this helper replaces dozens of arbitrary sleeps across a typical test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It Works So Well
&lt;/h2&gt;

&lt;p&gt;Instead of writing:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you simply write:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;eventually&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;orderCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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;If the job completes after 800 milliseconds, the test finishes after 800 milliseconds.&lt;/p&gt;

&lt;p&gt;If it needs four seconds, the helper patiently waits.&lt;/p&gt;

&lt;p&gt;No guessing required.&lt;/p&gt;




&lt;h1&gt;
  
  
  Asserting on Side Effects: The Row, the Event, the Cache
&lt;/h1&gt;

&lt;p&gt;One common mistake is checking only the API response.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /orders
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;202 Accepted
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many tests stop there.&lt;/p&gt;

&lt;p&gt;That tells you only that the message entered the queue.&lt;/p&gt;

&lt;p&gt;It says nothing about whether processing succeeded.&lt;/p&gt;

&lt;p&gt;Instead, verify the side effects.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Database Changes
&lt;/h2&gt;

&lt;p&gt;Example:&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;eventually&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&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;order&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="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Completed&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;h2&gt;
  
  
  2. Published Events
&lt;/h2&gt;

&lt;p&gt;Many async systems emit events.&lt;/p&gt;

&lt;p&gt;Verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event exists.&lt;/li&gt;
&lt;li&gt;Payload is correct.&lt;/li&gt;
&lt;li&gt;Event type matches expectations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCompleted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should appear exactly once.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Cache Updates
&lt;/h2&gt;

&lt;p&gt;Suppose order summaries are cached.&lt;/p&gt;

&lt;p&gt;After processing completes:&lt;/p&gt;

&lt;p&gt;Verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache exists.&lt;/li&gt;
&lt;li&gt;Cached values are correct.&lt;/li&gt;
&lt;li&gt;Stale entries disappeared.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring cache validation often hides production bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Notifications
&lt;/h2&gt;

&lt;p&gt;If background jobs send:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Emails&lt;/li&gt;
&lt;li&gt;SMS&lt;/li&gt;
&lt;li&gt;Push notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test the message queue or mock notification service rather than relying solely on database assertions.&lt;/p&gt;




&lt;h1&gt;
  
  
  Choosing a Timeout Strategy
&lt;/h1&gt;

&lt;p&gt;The next question becomes:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"How long should eventually() wait?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Many teams guess.&lt;/p&gt;

&lt;p&gt;I recommend using production metrics instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measure the 99th Percentile
&lt;/h2&gt;

&lt;p&gt;Suppose monitoring shows:&lt;/p&gt;

&lt;p&gt;Average:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;95th percentile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;900 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;99th percentile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.8 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose a timeout around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 × P99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5–6 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides enough tolerance for occasional variance without masking genuine performance regressions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Infinite Retries?
&lt;/h2&gt;

&lt;p&gt;Infinite retries create dangerous tests.&lt;/p&gt;

&lt;p&gt;A failed job should fail the pipeline—not wait forever.&lt;/p&gt;

&lt;p&gt;A timeout communicates:&lt;/p&gt;

&lt;p&gt;"This condition never became true."&lt;/p&gt;

&lt;p&gt;That's valuable debugging information.&lt;/p&gt;




&lt;h1&gt;
  
  
  Testing Job Retries
&lt;/h1&gt;

&lt;p&gt;Many queues automatically retry failed jobs.&lt;/p&gt;

&lt;p&gt;Those retries deserve explicit tests.&lt;/p&gt;

&lt;p&gt;Suppose processing fails because an external API is temporarily unavailable.&lt;/p&gt;

&lt;p&gt;Expected behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1

↓

Failure

↓

Retry

↓

Attempt 2

↓

Success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your test should verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry count.&lt;/li&gt;
&lt;li&gt;Retry delay.&lt;/li&gt;
&lt;li&gt;Final success.&lt;/li&gt;
&lt;li&gt;No duplicate side effects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retries often introduce subtle bugs such as duplicate database writes or duplicate notifications.&lt;/p&gt;




&lt;h1&gt;
  
  
  Testing Dead Letter Queues (DLQs)
&lt;/h1&gt;

&lt;p&gt;Some failures should never succeed.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid payload&lt;/li&gt;
&lt;li&gt;Corrupt message&lt;/li&gt;
&lt;li&gt;Missing required fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After repeated retries:&lt;/p&gt;

&lt;p&gt;The message should move into the Dead Letter Queue.&lt;/p&gt;

&lt;p&gt;Test expectations include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry limit reached.&lt;/li&gt;
&lt;li&gt;DLQ contains message.&lt;/li&gt;
&lt;li&gt;Original queue is empty.&lt;/li&gt;
&lt;li&gt;Error logged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring DLQ behavior leaves one of the most important resilience mechanisms completely untested.&lt;/p&gt;




&lt;h1&gt;
  
  
  Common Mistakes in Async API Testing
&lt;/h1&gt;

&lt;p&gt;Over time, I've seen the same patterns repeatedly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Fixed Sleeps
&lt;/h3&gt;

&lt;p&gt;Creates slow and unreliable pipelines.&lt;/p&gt;




&lt;h3&gt;
  
  
  Verifying Only HTTP Responses
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;202 Accepted&lt;/code&gt; response does not guarantee successful processing.&lt;/p&gt;




&lt;h3&gt;
  
  
  Ignoring Side Effects
&lt;/h3&gt;

&lt;p&gt;Database updates, cache invalidation, and events deserve verification.&lt;/p&gt;




&lt;h3&gt;
  
  
  Skipping Retry Logic
&lt;/h3&gt;

&lt;p&gt;Retries often behave differently from first attempts.&lt;/p&gt;




&lt;h3&gt;
  
  
  Never Testing Failure Paths
&lt;/h3&gt;

&lt;p&gt;DLQs and permanent failures are part of the application—not edge cases.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Practical Async Testing Checklist
&lt;/h1&gt;

&lt;p&gt;For every asynchronous workflow, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the API return the expected acknowledgement?&lt;/li&gt;
&lt;li&gt;Did the background job finish?&lt;/li&gt;
&lt;li&gt;Was the database updated?&lt;/li&gt;
&lt;li&gt;Were downstream events published?&lt;/li&gt;
&lt;li&gt;Was the cache refreshed?&lt;/li&gt;
&lt;li&gt;Were retries handled correctly?&lt;/li&gt;
&lt;li&gt;Were permanent failures routed to the DLQ?&lt;/li&gt;
&lt;li&gt;Did everything complete within acceptable time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Answering these questions provides much stronger confidence than simply waiting five seconds and hoping the job finished.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Asynchronous systems introduce complexity that synchronous APIs simply don't have.&lt;/p&gt;

&lt;p&gt;The temptation to sprinkle &lt;code&gt;sleep()&lt;/code&gt; calls throughout the test suite is understandable, but those arbitrary delays almost always lead to slower pipelines, flaky builds, and difficult debugging sessions.&lt;/p&gt;

&lt;p&gt;A better approach is to wait for outcomes rather than time.&lt;/p&gt;

&lt;p&gt;Polling with a lightweight &lt;code&gt;eventually()&lt;/code&gt; helper allows tests to complete as soon as work finishes, while side-effect assertions ensure background jobs actually performed the expected business operations.&lt;/p&gt;

&lt;p&gt;Combined with sensible timeout strategies based on production metrics and explicit tests for retries and Dead Letter Queues, this creates a much more reliable approach to &lt;strong&gt;async API testing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of asking, &lt;em&gt;"Has enough time passed?"&lt;/em&gt;, your tests begin asking the more important question:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Has the expected outcome happened?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's the question your users ultimately care about—and your automation should too.&lt;/p&gt;

&lt;p&gt;If you're implementing asynchronous APIs with queues, events, or background workers, you'll find additional examples in &lt;strong&gt;&lt;a href="https://totalshiftleft.ai/blog/testing-message-queue-microservices" rel="noopener noreferrer"&gt;the async/queue testing pattern we documented&lt;/a&gt;&lt;/strong&gt;, including end-to-end workflows, queue validation techniques, and testing strategies for distributed microservices.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>api</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>API Error Codes: A Test Suite Pattern I Stole from Stripe</title>
      <dc:creator>Smeet Gohel</dc:creator>
      <pubDate>Fri, 26 Jun 2026 13:39:06 +0000</pubDate>
      <link>https://dev.to/smeetgohel/api-error-codes-a-test-suite-pattern-i-stole-from-stripe-20ce</link>
      <guid>https://dev.to/smeetgohel/api-error-codes-a-test-suite-pattern-i-stole-from-stripe-20ce</guid>
      <description>&lt;p&gt;&lt;em&gt;Read Stripe's API reference for an hour and you'll notice every endpoint has a complete enumerated list of error codes with example payloads. Then look at your own API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The contrast is hard to ignore.&lt;/p&gt;

&lt;p&gt;Stripe's API documentation treats errors as first-class citizens. Every endpoint clearly documents not only the happy path but also every expected failure, complete with structured error codes, descriptions, HTTP status codes, and example responses.&lt;/p&gt;

&lt;p&gt;Now compare that to many APIs in production.&lt;/p&gt;

&lt;p&gt;You might find a generic list of HTTP status codes somewhere in the documentation, but business-specific errors are often buried inside controller logic, scattered across wiki pages, or simply undocumented. The test suite isn't much better—there are dozens of happy-path tests, but only a handful of negative scenarios.&lt;/p&gt;

&lt;p&gt;That imbalance creates problems for everyone involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers don't know which errors are expected.&lt;/li&gt;
&lt;li&gt;Frontend teams can't reliably handle failures.&lt;/li&gt;
&lt;li&gt;QA engineers miss important negative cases.&lt;/li&gt;
&lt;li&gt;Refactoring accidentally changes error responses without anyone noticing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few years ago, I borrowed a simple idea from Stripe's documentation and turned it into a testing strategy.&lt;/p&gt;

&lt;p&gt;Instead of treating error responses as exceptions, we created an &lt;strong&gt;error-code catalog&lt;/strong&gt; and made it the foundation of our negative test suite.&lt;/p&gt;

&lt;p&gt;The result wasn't just better &lt;strong&gt;API error code testing&lt;/strong&gt;—it also improved documentation, simplified maintenance, and made API contracts far more consistent.&lt;/p&gt;

&lt;p&gt;Here's how the pattern works.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Error Responses Are Part of the API Contract
&lt;/h1&gt;

&lt;p&gt;When people think about API testing, they naturally focus on successful responses.&lt;/p&gt;

&lt;p&gt;Typical assertions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP 200 OK&lt;/li&gt;
&lt;li&gt;HTTP 201 Created&lt;/li&gt;
&lt;li&gt;Correct JSON payload&lt;/li&gt;
&lt;li&gt;Required fields&lt;/li&gt;
&lt;li&gt;Business calculations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Negative testing often gets much less attention.&lt;/p&gt;

&lt;p&gt;Maybe there are a few tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invalid authentication&lt;/li&gt;
&lt;li&gt;Missing required fields&lt;/li&gt;
&lt;li&gt;Unknown resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond that, many APIs rely on manual testing or hope that the framework handles everything correctly.&lt;/p&gt;

&lt;p&gt;The problem is that real users encounter failures just as often as successful requests.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer account locked&lt;/li&gt;
&lt;li&gt;Payment declined&lt;/li&gt;
&lt;li&gt;Coupon expired&lt;/li&gt;
&lt;li&gt;Inventory unavailable&lt;/li&gt;
&lt;li&gt;Duplicate registration&lt;/li&gt;
&lt;li&gt;Subscription canceled&lt;/li&gt;
&lt;li&gt;Rate limit exceeded&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't exceptional scenarios.&lt;/p&gt;

&lt;p&gt;They're expected business outcomes.&lt;/p&gt;

&lt;p&gt;Treating them as first-class API contracts changes how you design both documentation and tests.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Error-Code Catalog as a Test Input
&lt;/h1&gt;

&lt;p&gt;The first step is creating a centralized catalog of every business error the API can intentionally return.&lt;/p&gt;

&lt;p&gt;A simplified example might look like this:&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;errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;USER_NOT_FOUND&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User not found&lt;/span&gt;

  &lt;span class="na"&gt;EMAIL_ALREADY_EXISTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;409&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email already exists&lt;/span&gt;

  &lt;span class="na"&gt;INVALID_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Invalid authentication token&lt;/span&gt;

  &lt;span class="na"&gt;PAYMENT_DECLINED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;402&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Payment declined&lt;/span&gt;

  &lt;span class="na"&gt;ORDER_ALREADY_SHIPPED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;409&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Order cannot be modified&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catalog becomes far more than documentation.&lt;/p&gt;

&lt;p&gt;It becomes an executable specification.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What errors should this endpoint return?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;the answer already exists in one authoritative location.&lt;/p&gt;

&lt;p&gt;Every new business error must be added here before it reaches production.&lt;/p&gt;

&lt;p&gt;That single requirement dramatically improves consistency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a Catalog Helps
&lt;/h2&gt;

&lt;p&gt;Without a catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documentation drifts.&lt;/li&gt;
&lt;li&gt;Tests become incomplete.&lt;/li&gt;
&lt;li&gt;Frontend teams discover errors by accident.&lt;/li&gt;
&lt;li&gt;Reviewers overlook breaking changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every error is documented.&lt;/li&gt;
&lt;li&gt;Every error becomes testable.&lt;/li&gt;
&lt;li&gt;Every API consumer sees the same contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catalog becomes the foundation for automation.&lt;/p&gt;




&lt;h1&gt;
  
  
  One Test per Error Code, Generated from the Catalog
&lt;/h1&gt;

&lt;p&gt;Once the catalog exists, generating negative tests becomes surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;Rather than manually writing dozens of repetitive tests, a generator simply iterates through every defined error.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="k"&gt;for &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;errorCode&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;generateNegativeTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorCode&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;Each generated test validates four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The expected HTTP status&lt;/li&gt;
&lt;li&gt;The error code&lt;/li&gt;
&lt;li&gt;The error message&lt;/li&gt;
&lt;li&gt;The response schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider &lt;code&gt;EMAIL_ALREADY_EXISTS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The generated scenario might:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a user.&lt;/li&gt;
&lt;li&gt;Attempt to create the same user again.&lt;/li&gt;
&lt;li&gt;Verify the response:
&lt;/li&gt;
&lt;/ol&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMAIL_ALREADY_EXISTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Email already exists"&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;The implementation differs depending on the framework, but the testing philosophy remains the same:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every documented error deserves exactly one corresponding test.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As new error codes are introduced, new tests appear automatically.&lt;/p&gt;

&lt;p&gt;No engineer has to remember to write them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Scales Better
&lt;/h2&gt;

&lt;p&gt;Imagine your API exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;150 endpoints&lt;/li&gt;
&lt;li&gt;90 business error codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Maintaining those manually quickly becomes tedious.&lt;/p&gt;

&lt;p&gt;Generation solves two maintenance problems simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing tests&lt;/li&gt;
&lt;li&gt;Duplicate effort&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of asking developers to remember every negative case, the catalog guarantees baseline coverage.&lt;/p&gt;

&lt;p&gt;Engineers can then focus on more complex business workflows rather than repetitive validation tests.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Shape Assertion That Prevents Silent Error Drift
&lt;/h1&gt;

&lt;p&gt;One lesson we learned very early was this:&lt;/p&gt;

&lt;p&gt;Checking only the HTTP status is almost useless.&lt;/p&gt;

&lt;p&gt;Imagine an endpoint originally returns:&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_NOT_FOUND"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&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;Months later, someone refactors the global exception handler.&lt;/p&gt;

&lt;p&gt;The response becomes:&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User not found"&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;The HTTP status is still:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many tests still pass.&lt;/p&gt;

&lt;p&gt;But every client expecting the original response contract is now broken.&lt;/p&gt;

&lt;p&gt;This is known as &lt;strong&gt;silent error drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing appears wrong until consumers start failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Shape Assertions
&lt;/h2&gt;

&lt;p&gt;Every negative test also validates the response structure.&lt;/p&gt;

&lt;p&gt;Example:&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="nf"&gt;expect&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;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&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;Notice that we're not only validating values.&lt;/p&gt;

&lt;p&gt;We're validating the schema itself.&lt;/p&gt;

&lt;p&gt;That single assertion protects every API consumer from accidental response changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Consumers often depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Error codes&lt;/li&gt;
&lt;li&gt;Localization keys&lt;/li&gt;
&lt;li&gt;Correlation IDs&lt;/li&gt;
&lt;li&gt;Documentation URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Removing any of these fields can become a breaking API change even though the HTTP status remains correct.&lt;/p&gt;

&lt;p&gt;Schema validation catches those problems immediately.&lt;/p&gt;




&lt;h1&gt;
  
  
  Keeping the Catalog in Sync with the Code (Code Generation)
&lt;/h1&gt;

&lt;p&gt;The obvious concern is maintenance.&lt;/p&gt;

&lt;p&gt;If engineers must manually update both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source code&lt;/li&gt;
&lt;li&gt;Error catalog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the catalog eventually becomes outdated.&lt;/p&gt;

&lt;p&gt;The solution is code generation.&lt;/p&gt;

&lt;p&gt;Most applications already define errors centrally.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;enum&lt;/span&gt; &lt;span class="nx"&gt;ErrorCode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;USER_NOT_FOUND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;INVALID_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;PAYMENT_DECLINED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;EMAIL_ALREADY_EXISTS&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple generation step can produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API documentation&lt;/li&gt;
&lt;li&gt;OpenAPI components&lt;/li&gt;
&lt;li&gt;Markdown reference tables&lt;/li&gt;
&lt;li&gt;Test inputs&lt;/li&gt;
&lt;li&gt;SDK constants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All from the same source.&lt;/p&gt;

&lt;p&gt;Now there's only one place where error definitions live.&lt;/p&gt;

&lt;p&gt;Everything else is generated automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benefits of Codegen
&lt;/h2&gt;

&lt;p&gt;This approach creates several advantages:&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation Never Falls Behind
&lt;/h3&gt;

&lt;p&gt;As soon as a new error appears in code, documentation updates automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generated Tests Stay Current
&lt;/h3&gt;

&lt;p&gt;No manual synchronization required.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Consumers Stay Aligned
&lt;/h3&gt;

&lt;p&gt;Client SDKs can reference the same constants used by the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Reviews Become Easier
&lt;/h3&gt;

&lt;p&gt;Adding a new business error becomes highly visible because it affects generated documentation and tests.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Two Error Codes We Deliberately Don't Test (And Why)
&lt;/h1&gt;

&lt;p&gt;Although our negative suite covers nearly every business error, there are two categories we intentionally exclude.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Generic Internal Server Errors
&lt;/h2&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;500 Internal Server Error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These represent unexpected failures.&lt;/p&gt;

&lt;p&gt;They're not part of normal business behavior.&lt;/p&gt;

&lt;p&gt;Rather than intentionally triggering every possible internal exception, we verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sensitive details aren't exposed&lt;/li&gt;
&lt;li&gt;Generic messages are returned&lt;/li&gt;
&lt;li&gt;Correlation IDs exist&lt;/li&gt;
&lt;li&gt;Logging occurs correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing every possible server failure adds little value.&lt;/p&gt;

&lt;p&gt;Testing the response contract provides much greater return.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Infrastructure Failures
&lt;/h2&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database unavailable&lt;/li&gt;
&lt;li&gt;Network partition&lt;/li&gt;
&lt;li&gt;DNS outage&lt;/li&gt;
&lt;li&gt;Message broker failure&lt;/li&gt;
&lt;li&gt;Cloud storage unavailable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These failures belong to resilience testing rather than standard API automation.&lt;/p&gt;

&lt;p&gt;They are better validated using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chaos engineering&lt;/li&gt;
&lt;li&gt;Fault injection&lt;/li&gt;
&lt;li&gt;Infrastructure testing&lt;/li&gt;
&lt;li&gt;Disaster recovery exercises&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mixing infrastructure scenarios into routine &lt;strong&gt;API negative tests&lt;/strong&gt; usually creates unstable pipelines.&lt;/p&gt;

&lt;p&gt;Keeping them separate results in cleaner and more reliable automation.&lt;/p&gt;




&lt;h1&gt;
  
  
  Additional Benefits We Didn't Expect
&lt;/h1&gt;

&lt;p&gt;Once the catalog became part of our development process, several unexpected improvements appeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  More Consistent APIs
&lt;/h3&gt;

&lt;p&gt;Every endpoint used the same response format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better Frontend Development
&lt;/h3&gt;

&lt;p&gt;Frontend teams no longer guessed which errors could occur.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simpler Documentation
&lt;/h3&gt;

&lt;p&gt;Error references stayed synchronized automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaner Pull Requests
&lt;/h3&gt;

&lt;p&gt;Adding a new error became an explicit design decision rather than an implementation detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better QA Coverage
&lt;/h3&gt;

&lt;p&gt;Negative scenarios became just as visible as successful ones.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Most engineering teams invest heavily in testing successful requests while treating failures as secondary concerns.&lt;/p&gt;

&lt;p&gt;Stripe demonstrates a different philosophy.&lt;/p&gt;

&lt;p&gt;Errors are documented, standardized, and treated as an integral part of the public API contract.&lt;/p&gt;

&lt;p&gt;Building an error-code catalog allowed us to adopt that same mindset.&lt;/p&gt;

&lt;p&gt;Instead of manually maintaining dozens of repetitive &lt;strong&gt;error response testing&lt;/strong&gt; scenarios, we generated them from a single source of truth.&lt;/p&gt;

&lt;p&gt;Combined with response schema validation and code generation, the approach dramatically reduced maintenance while increasing confidence that every documented failure behaved exactly as expected.&lt;/p&gt;

&lt;p&gt;If your API already has a growing collection of business errors, consider creating a centralized catalog before the list becomes unmanageable.&lt;/p&gt;

&lt;p&gt;The investment is relatively small, but the payoff in documentation quality, test coverage, and long-term maintainability is substantial.&lt;/p&gt;

&lt;p&gt;If you'd like to explore how automated API testing can support this approach, you can &lt;strong&gt;&lt;a href="https://totalshiftleft.ai/free-trial-signup" rel="noopener noreferrer"&gt;spin up a free trial to try this catalog pattern&lt;/a&gt;&lt;/strong&gt; and see how generated negative tests, schema validation, and API contracts work together in practice.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>testing</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>5 OpenAPI Mistakes That Break Every Test Generator I've Tried</title>
      <dc:creator>Smeet Gohel</dc:creator>
      <pubDate>Thu, 25 Jun 2026 13:28:37 +0000</pubDate>
      <link>https://dev.to/smeetgohel/5-openapi-mistakes-that-break-every-test-generator-ive-tried-f2b</link>
      <guid>https://dev.to/smeetgohel/5-openapi-mistakes-that-break-every-test-generator-ive-tried-f2b</guid>
      <description>&lt;p&gt;&lt;em&gt;I fed the same OpenAPI spec into four different test generators last weekend. All four failed on the same five things.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That wasn't supposed to happen.&lt;/p&gt;

&lt;p&gt;The generators came from different vendors. They used different parsing engines, different test-generation strategies, and different AI capabilities. Some focused on contract testing. Others emphasized intelligent test creation and edge-case discovery.&lt;/p&gt;

&lt;p&gt;Yet all four stumbled on the exact same sections of the specification.&lt;/p&gt;

&lt;p&gt;The surprising part wasn't that the tools failed.&lt;/p&gt;

&lt;p&gt;The surprising part was that every failure could be traced back to issues inside the OpenAPI document itself.&lt;/p&gt;

&lt;p&gt;Over the years, I've learned that most API test generation problems aren't actually tool problems. They're specification quality problems.&lt;/p&gt;

&lt;p&gt;A clean OpenAPI specification can generate hundreds of useful tests automatically.&lt;/p&gt;

&lt;p&gt;A flawed specification can confuse even the smartest generator.&lt;/p&gt;

&lt;p&gt;If you're using OpenAPI to generate tests, SDKs, mocks, documentation, or validation suites, these are the five most common &lt;strong&gt;OpenAPI mistakes&lt;/strong&gt; I've seen repeatedly—and why they cause so much trouble.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Missing Required Fields on Nested &lt;code&gt;$ref&lt;/code&gt; Objects
&lt;/h2&gt;

&lt;p&gt;This is easily one of the most overlooked issues in OpenAPI specifications.&lt;/p&gt;

&lt;p&gt;Consider the following schema:&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;Customer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Address'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The referenced schema looks like:&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;Address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;street&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;city&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks harmless.&lt;/p&gt;

&lt;p&gt;Now imagine the actual API requires:&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;"street"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Main Street"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"London"&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;but the schema never specifies:&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;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;street&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;city&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator now assumes both fields are optional.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Test Generators Struggle
&lt;/h3&gt;

&lt;p&gt;When generating positive and negative test cases, the tool needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which fields must exist&lt;/li&gt;
&lt;li&gt;Which fields may be omitted&lt;/li&gt;
&lt;li&gt;Which omissions should trigger validation failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without required declarations, generators often create:&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;"address"&lt;/span&gt;&lt;span class="p"&gt;:&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;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;and treat the payload as valid.&lt;/p&gt;

&lt;p&gt;The resulting tests provide little value because they don't reflect real application behavior.&lt;/p&gt;




&lt;h3&gt;
  
  
  Best Practice
&lt;/h3&gt;

&lt;p&gt;Always define required fields explicitly at every schema level.&lt;/p&gt;

&lt;p&gt;Even when schemas are reused via &lt;code&gt;$ref&lt;/code&gt;, each referenced object should clearly identify mandatory properties.&lt;/p&gt;

&lt;p&gt;Never assume the generator will infer business intent.&lt;/p&gt;

&lt;p&gt;Schemas should be unambiguous.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. &lt;code&gt;anyOf&lt;/code&gt; With No Discriminator (The One That Hurts Most)
&lt;/h2&gt;

&lt;p&gt;If I had to choose the single most problematic specification pattern, this would be it.&lt;/p&gt;

&lt;p&gt;Consider:&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;Pet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;anyOf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Cat'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Dog'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the generator:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The payload may match Cat or Dog."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sounds reasonable.&lt;/p&gt;

&lt;p&gt;The problem is that the generator has no reliable way to determine which schema should be used for a specific test case.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why It Breaks Generation
&lt;/h3&gt;

&lt;p&gt;Imagine:&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;Cat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;properties&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="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="na"&gt;Dog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;properties&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="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the payload:&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;"Buddy"&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;matches both schemas.&lt;/p&gt;

&lt;p&gt;The generator faces several questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this a Cat?&lt;/li&gt;
&lt;li&gt;Is this a Dog?&lt;/li&gt;
&lt;li&gt;Should both test paths be created?&lt;/li&gt;
&lt;li&gt;Which validation rules apply?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different tools make different assumptions.&lt;/p&gt;

&lt;p&gt;Most assumptions are wrong.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;Use discriminators whenever possible.&lt;/p&gt;

&lt;p&gt;Example:&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;Pet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;oneOf&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Cat'&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Dog'&lt;/span&gt;
  &lt;span class="na"&gt;discriminator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;propertyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;petType&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now responses become:&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;"petType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dog"&lt;/span&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;"Buddy"&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;The ambiguity disappears.&lt;/p&gt;

&lt;p&gt;Test generation becomes deterministic.&lt;/p&gt;

&lt;p&gt;This single change often improves generated coverage dramatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Untyped Query Parameters (String vs Integer Ambiguity)
&lt;/h2&gt;

&lt;p&gt;Another surprisingly common issue involves query parameters.&lt;/p&gt;

&lt;p&gt;Consider:&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;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&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;page&lt;/span&gt;
    &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks simple.&lt;/p&gt;

&lt;p&gt;Unfortunately, the schema never specifies the data type.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why This Creates Problems
&lt;/h3&gt;

&lt;p&gt;A generator needs type information to create meaningful tests.&lt;/p&gt;

&lt;p&gt;Should it generate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?page=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?page=abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?page=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without explicit typing, every option becomes theoretically valid.&lt;/p&gt;

&lt;p&gt;Different generators handle this differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some assume strings&lt;/li&gt;
&lt;li&gt;Some guess based on naming&lt;/li&gt;
&lt;li&gt;Some generate everything&lt;/li&gt;
&lt;li&gt;Some skip validation entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these approaches are ideal.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Hidden Impact
&lt;/h3&gt;

&lt;p&gt;This issue affects more than positive tests.&lt;/p&gt;

&lt;p&gt;It also impacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boundary testing&lt;/li&gt;
&lt;li&gt;Negative testing&lt;/li&gt;
&lt;li&gt;Fuzz testing&lt;/li&gt;
&lt;li&gt;Schema validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&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;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
  &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;enables generators to automatically create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0&lt;/li&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;100&lt;/li&gt;
&lt;li&gt;101&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without typing, those valuable edge cases disappear.&lt;/p&gt;




&lt;h3&gt;
  
  
  Best Practice
&lt;/h3&gt;

&lt;p&gt;Always define parameter schemas completely.&lt;/p&gt;

&lt;p&gt;Example:&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;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&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;page&lt;/span&gt;
    &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
      &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The more constraints provided, the better the generated tests become.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Example Values That Don't Match the Schema
&lt;/h2&gt;

&lt;p&gt;This problem creates some of the most confusing failures.&lt;/p&gt;

&lt;p&gt;Imagine:&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
&lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
&lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The examples look correct at first glance.&lt;/p&gt;

&lt;p&gt;But they violate the schema definition.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;Most generators rely heavily on example values.&lt;/p&gt;

&lt;p&gt;Examples help generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request payloads&lt;/li&gt;
&lt;li&gt;Mock data&lt;/li&gt;
&lt;li&gt;Positive test cases&lt;/li&gt;
&lt;li&gt;Sample assertions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When examples contradict schemas, the generator receives conflicting instructions.&lt;/p&gt;

&lt;p&gt;The schema says:&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example says:&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which one should the tool trust?&lt;/p&gt;




&lt;h3&gt;
  
  
  What Usually Happens
&lt;/h3&gt;

&lt;p&gt;Different generators respond differently:&lt;/p&gt;

&lt;h4&gt;
  
  
  Generator A
&lt;/h4&gt;

&lt;p&gt;Uses schema definition.&lt;/p&gt;

&lt;h4&gt;
  
  
  Generator B
&lt;/h4&gt;

&lt;p&gt;Uses example value.&lt;/p&gt;

&lt;h4&gt;
  
  
  Generator C
&lt;/h4&gt;

&lt;p&gt;Attempts to coerce data types.&lt;/p&gt;

&lt;h4&gt;
  
  
  Generator D
&lt;/h4&gt;

&lt;p&gt;Fails validation completely.&lt;/p&gt;

&lt;p&gt;The result is inconsistent behavior across platforms.&lt;/p&gt;




&lt;h3&gt;
  
  
  Best Practice
&lt;/h3&gt;

&lt;p&gt;Treat examples as executable documentation.&lt;/p&gt;

&lt;p&gt;Every example should validate successfully against its schema.&lt;/p&gt;

&lt;p&gt;A useful review process is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate schema&lt;/li&gt;
&lt;li&gt;Validate examples&lt;/li&gt;
&lt;li&gt;Validate generated payloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three should agree.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Auth Scheme Defined Globally but Overridden Per-Path
&lt;/h2&gt;

&lt;p&gt;Authentication definitions often create subtle specification issues.&lt;/p&gt;

&lt;p&gt;Consider:&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;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;bearerAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This applies globally.&lt;/p&gt;

&lt;p&gt;Everything looks fine.&lt;/p&gt;

&lt;p&gt;Later, a specific endpoint introduces:&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;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/public-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This explicitly removes authentication.&lt;/p&gt;

&lt;p&gt;Still valid.&lt;/p&gt;

&lt;p&gt;The trouble begins when specifications contain dozens or hundreds of endpoints.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Test Generators Fail Here
&lt;/h3&gt;

&lt;p&gt;Generators must determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which endpoints require authentication&lt;/li&gt;
&lt;li&gt;Which endpoints are public&lt;/li&gt;
&lt;li&gt;Which credentials to attach&lt;/li&gt;
&lt;li&gt;Which negative auth scenarios to generate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conflicting security definitions make this surprisingly difficult.&lt;/p&gt;

&lt;p&gt;I've seen generators:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add tokens to public endpoints&lt;/li&gt;
&lt;li&gt;Skip tokens on protected endpoints&lt;/li&gt;
&lt;li&gt;Generate invalid authentication tests&lt;/li&gt;
&lt;li&gt;Ignore overrides completely&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Real Problem
&lt;/h3&gt;

&lt;p&gt;Many teams inherit APIs over several years.&lt;/p&gt;

&lt;p&gt;Security definitions evolve.&lt;/p&gt;

&lt;p&gt;Documentation evolves.&lt;/p&gt;

&lt;p&gt;Endpoints move between versions.&lt;/p&gt;

&lt;p&gt;Eventually, the specification contains multiple overlapping security patterns.&lt;/p&gt;

&lt;p&gt;Humans can usually understand the intent.&lt;/p&gt;

&lt;p&gt;Generators often cannot.&lt;/p&gt;




&lt;h3&gt;
  
  
  Best Practice
&lt;/h3&gt;

&lt;p&gt;Maintain a clear authentication strategy.&lt;/p&gt;

&lt;p&gt;Review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global security settings&lt;/li&gt;
&lt;li&gt;Path-level overrides&lt;/li&gt;
&lt;li&gt;Operation-level overrides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever possible, minimize exceptions.&lt;/p&gt;

&lt;p&gt;The fewer special cases you create, the easier automated tooling becomes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why These Mistakes Keep Appearing
&lt;/h2&gt;

&lt;p&gt;What's interesting is that none of these issues are technically invalid OpenAPI.&lt;/p&gt;

&lt;p&gt;Many specifications containing these patterns will still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render documentation correctly&lt;/li&gt;
&lt;li&gt;Generate SDKs successfully&lt;/li&gt;
&lt;li&gt;Pass schema validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problems emerge only when advanced automation enters the picture.&lt;/p&gt;

&lt;p&gt;Test generation requires much higher precision than documentation generation.&lt;/p&gt;

&lt;p&gt;Documentation can tolerate ambiguity.&lt;/p&gt;

&lt;p&gt;Automated testing cannot.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Simple Validation Checklist
&lt;/h2&gt;

&lt;p&gt;Before feeding an OpenAPI document into any generator, review the following:&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema Definitions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are required fields defined?&lt;/li&gt;
&lt;li&gt;Are nested references complete?&lt;/li&gt;
&lt;li&gt;Are enums constrained properly?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Polymorphism
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Does every &lt;code&gt;anyOf&lt;/code&gt; or &lt;code&gt;oneOf&lt;/code&gt; include a discriminator?&lt;/li&gt;
&lt;li&gt;Can payload types be identified deterministically?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are query parameters typed?&lt;/li&gt;
&lt;li&gt;Are constraints defined?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Do examples validate against schemas?&lt;/li&gt;
&lt;li&gt;Are example payloads realistic?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Are security rules consistent?&lt;/li&gt;
&lt;li&gt;Are overrides intentional?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This five-minute review can save hours of debugging later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;After testing multiple generators against the same API specification, one conclusion became obvious:&lt;/p&gt;

&lt;p&gt;Most test-generation failures begin long before the generator runs.&lt;/p&gt;

&lt;p&gt;They begin when the specification is written.&lt;/p&gt;

&lt;p&gt;The better your OpenAPI document, the better your generated tests, mocks, SDKs, documentation, and validation suites become.&lt;/p&gt;

&lt;p&gt;Tools continue to improve every year.&lt;/p&gt;

&lt;p&gt;AI-assisted generation is becoming increasingly capable.&lt;/p&gt;

&lt;p&gt;Yet even the most advanced platform struggles when the specification contains ambiguity, conflicting definitions, or incomplete schema information.&lt;/p&gt;

&lt;p&gt;If you're &lt;a href="https://totalshiftleft.ai/blog/how-to-generate-api-tests-from-openapi" rel="noopener noreferrer"&gt;planning to generate automated API tests&lt;/a&gt; from your OpenAPI specification, ensuring your specification is well-designed and complete is one of the most valuable investments you can make. To learn the best practices, avoid common pitfalls, and generate reliable API tests.&lt;/p&gt;

&lt;p&gt;A clean specification doesn't just improve documentation.&lt;/p&gt;

&lt;p&gt;It becomes the foundation for every automation layer built on top of it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>api</category>
      <category>postman</category>
    </item>
  </channel>
</rss>
