<?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: Goqa Ai</title>
    <description>The latest articles on DEV Community by Goqa Ai (@goqa_ai_c97f712237a7383a5).</description>
    <link>https://dev.to/goqa_ai_c97f712237a7383a5</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%2F3993447%2F0a5376de-79bb-471d-80d8-c7335544ba5a.png</url>
      <title>DEV Community: Goqa Ai</title>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goqa_ai_c97f712237a7383a5"/>
    <language>en</language>
    <item>
      <title>What good Gherkin looks like, and the four ways it goes wrong</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:18:22 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/what-good-gherkin-looks-like-and-the-four-ways-it-goes-wrong-2mm6</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/what-good-gherkin-looks-like-and-the-four-ways-it-goes-wrong-2mm6</guid>
      <description>&lt;p&gt;Gherkin gets a bad reputation from teams who adopted it, wrote scenarios nobody could read, and concluded the format was the problem. It usually isn't. Here are the four failure modes and what to do instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 1: Gherkin as a UI script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bad&lt;/span&gt;
&lt;span class="nf"&gt;When &lt;/span&gt;I click the button with id &lt;span class="s"&gt;"submit-btn"&lt;/span&gt;
&lt;span class="nf"&gt;And &lt;/span&gt;I wait 2 seconds
&lt;span class="nf"&gt;And &lt;/span&gt;I click the third row in the table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks when the button gets renamed, and it describes mechanics rather than behaviour. Six months later nobody can tell what it was protecting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Better&lt;/span&gt;
&lt;span class="nf"&gt;When &lt;/span&gt;the customer submits the order
&lt;span class="nf"&gt;Then &lt;/span&gt;the order appears in their history with status &lt;span class="s"&gt;"pending"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: &lt;strong&gt;Gherkin describes what the user achieves, the step definitions describe how.&lt;/strong&gt; If your feature file mentions a CSS selector, the abstraction has leaked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 2: Given as a setup dumping ground
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bad&lt;/span&gt;
&lt;span class="nf"&gt;Given &lt;/span&gt;a user exists
&lt;span class="nf"&gt;And &lt;/span&gt;the user has an email
&lt;span class="nf"&gt;And &lt;/span&gt;the user is verified
&lt;span class="nf"&gt;And &lt;/span&gt;the user has a subscription
&lt;span class="nf"&gt;And &lt;/span&gt;the subscription is active
&lt;span class="nf"&gt;And &lt;/span&gt;the subscription has 5 seats
&lt;span class="nf"&gt;And &lt;/span&gt;3 seats are used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody reads this. Collapse it into one meaningful state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Better&lt;/span&gt;
&lt;span class="nf"&gt;Given &lt;/span&gt;an active subscription with 3 of 5 seats used
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail lives in the step definition, where it belongs. A &lt;code&gt;Given&lt;/code&gt; should name a situation, not enumerate a database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 3: Multiple Whens
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bad&lt;/span&gt;
&lt;span class="nf"&gt;When &lt;/span&gt;the user adds an item
&lt;span class="nf"&gt;And &lt;/span&gt;the user applies a discount code
&lt;span class="nf"&gt;And &lt;/span&gt;the user checks out
&lt;span class="nf"&gt;Then &lt;/span&gt;the total is correct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three triggers means three behaviours means the failure tells you nothing about which one broke. And 'the total is correct' is not an assertion — correct according to what?&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;When&lt;/code&gt; per scenario. If you genuinely need a sequence, the earlier steps are &lt;code&gt;Given&lt;/code&gt;s — they are setup, not the thing under test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 4: Then with no observable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Bad&lt;/span&gt;
&lt;span class="nf"&gt;Then &lt;/span&gt;the system processes the payment
&lt;span class="nf"&gt;Then &lt;/span&gt;the data is saved correctly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither is observable. 'Processes' and 'correctly' cannot fail in any specific way, so the step definition ends up asserting something arbitrary, and the scenario passes forever regardless of behaviour.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="c"&gt;# Better&lt;/span&gt;
&lt;span class="nf"&gt;Then &lt;/span&gt;the order status is &lt;span class="s"&gt;"paid"&lt;/span&gt;
&lt;span class="nf"&gt;And &lt;/span&gt;a receipt is sent to the customer's email address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The shape that works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; discount code cannot be applied twice
  &lt;span class="nf"&gt;Given &lt;/span&gt;an order with the code SAVE10 already applied
  &lt;span class="nf"&gt;When &lt;/span&gt;the customer applies SAVE10 again
  &lt;span class="nf"&gt;Then &lt;/span&gt;the order total is unchanged
  &lt;span class="nf"&gt;And &lt;/span&gt;the customer is told the code is already applied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One situation. One trigger. Two observable outcomes. Readable by someone who has never seen the codebase — which is the actual test of whether your Gherkin is any good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where generation helps, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Once criteria are in this shape, enumerating the surrounding cases is mechanical: the expired code, the invalid code, the code that applies to no items in the basket, the code used by a different customer. That is what a generator is genuinely good at — GoQA produces these from criteria and exports them to Gherkin, Playwright, or Cypress.&lt;/p&gt;

&lt;p&gt;What it will not do is decide that 'used by a different customer' is the one carrying real risk. That is still your call, and it is the more important half.&lt;/p&gt;

</description>
      <category>agile</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>Deleting the option instead of defaulting it off</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:04:22 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/deleting-the-option-instead-of-defaulting-it-off-1ebm</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/deleting-the-option-instead-of-defaulting-it-off-1ebm</guid>
      <description>&lt;p&gt;We built a headless browser worker that renders our own pages for screenshots and link checks. Here is the design decision I keep coming back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual approach
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ProxyConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// defaults to none&lt;/span&gt;
  &lt;span class="nl"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// defaults to none&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine, and it is what most libraries do. The proxy is off unless you set it. The credentials are absent unless you supply them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we did not do that
&lt;/h2&gt;

&lt;p&gt;A configuration option that must never be set is a configuration option that will eventually be set.&lt;/p&gt;

&lt;p&gt;Not maliciously. Someone six months from now has a legitimate-sounding reason — a rate limit, a geo-restricted page, a login wall on a page we genuinely own — and the option is right there with a sensible-looking type. The code review is one line. Nobody remembers the argument that produced the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we shipped instead
&lt;/h2&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;interface&lt;/span&gt; &lt;span class="nx"&gt;ContextOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;egressAllowlist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;Four fields. There is no proxy field and no credential field anywhere in the package. A driver implementation cannot offer what the interface never asks for.&lt;/p&gt;

&lt;p&gt;There is a test asserting the exact shape, so adding a fifth field is a deliberate act with a failing test attached rather than an autocomplete accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The allowlist is not a default either
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;egressAllowlist&lt;/code&gt; is required, and it is derived from the worker's owned-surface policy rather than passed by the caller — so a task cannot widen its own reach. Navigation outside it is refused before a browsing context is created, and every subresource is failed at request time:&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;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Fetch.requestPaused&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nf"&gt;hostAllowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;continueRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;failRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BlockedByClient&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;That second half matters more than it looks. Without it, rendering a page of ours that embeds a third-party script means our worker makes that third-party request. The allowlist would be true of navigation and false of everything the page pulls in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost
&lt;/h2&gt;

&lt;p&gt;This is genuinely restrictive. The worker cannot log into anything, cannot render a page behind auth, and cannot follow a link off our own domain. Several plausible features are now impossible without an explicit, visible change to a shared interface.&lt;/p&gt;

&lt;p&gt;That is the trade. We decided the class of thing we want to be &lt;em&gt;unable&lt;/em&gt; to do is worth more than the features we are giving up.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this applies
&lt;/h2&gt;

&lt;p&gt;Not everywhere. Most options should be options. This pattern earns its cost when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The capability would be a terms violation, a privacy problem, or a security incident rather than a bug&lt;/li&gt;
&lt;li&gt;The pressure to enable it will arrive later, under a deadline, from someone who wasn't in this conversation&lt;/li&gt;
&lt;li&gt;The blast radius is other people rather than you&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all three are true, delete the field.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>design</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>A pragmatic accessibility checklist for teams with no a11y specialist</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:04:16 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/a-pragmatic-accessibility-checklist-for-teams-with-no-a11y-specialist-2k8c</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/a-pragmatic-accessibility-checklist-for-teams-with-no-a11y-specialist-2k8c</guid>
      <description>&lt;p&gt;WCAG 2.2 has 87 success criteria. If you are a four-person team shipping weekly, you are not going to work through 87 of anything. Here is the subset that catches most real harm, ordered by how often it goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Every interactive thing is reachable by keyboard
&lt;/h2&gt;

&lt;p&gt;Put the mouse away. Tab through a complete user journey.&lt;/p&gt;

&lt;p&gt;You are checking three things: can you reach every control, can you see which one has focus, and can you get out of everything you get into. A modal you can tab into but not out of is a trap, and it is common.&lt;/p&gt;

&lt;p&gt;The usual cause is a &lt;code&gt;div&lt;/code&gt; with a click handler. It has no tab stop and no role. Use a &lt;code&gt;button&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Every input has a real label
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- No. The label is decorative text that happens to sit nearby. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Yes. --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Placeholder text is not a label. It disappears when the user types, which is exactly when they need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Images say something useful, or nothing at all
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Meaningful image: &lt;code&gt;alt&lt;/code&gt; describes what it conveys, not what it depicts&lt;/li&gt;
&lt;li&gt;Decorative image: &lt;code&gt;alt=""&lt;/code&gt; — empty, present. A missing &lt;code&gt;alt&lt;/code&gt; makes screen readers read the filename&lt;/li&gt;
&lt;li&gt;Image of text: put the text in the page instead&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Contrast is 4.5:1 for body text
&lt;/h2&gt;

&lt;p&gt;AA needs 4.5:1 for normal text and 3:1 for large. The usual failure is muted grey body text — a design-system default nobody revisited. Devtools will tell you in the colour picker.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The heading outline makes sense on its own
&lt;/h2&gt;

&lt;p&gt;Screen-reader users navigate by headings the way you skim by eye. One &lt;code&gt;h1&lt;/code&gt;, no skipped levels, and heading tags used for structure rather than for font size.&lt;/p&gt;

&lt;p&gt;Read your headings in order with nothing else. If they do not describe the page, your outline is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Colour is never the only signal
&lt;/h2&gt;

&lt;p&gt;A red border on an invalid field is invisible to a colour-blind user, and to anyone on a washed-out screen in daylight. Add an icon, or text.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Focus is visible
&lt;/h2&gt;

&lt;p&gt;If you find &lt;code&gt;outline: none&lt;/code&gt; in your CSS with nothing replacing it, that is a WCAG 2.4.7 failure and a keyboard user is now navigating blind.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this list does not cover
&lt;/h2&gt;

&lt;p&gt;A lot. Live regions, complex widget semantics, reduced motion, cognitive load, and everything requiring judgement about whether your alt text is actually good.&lt;/p&gt;

&lt;p&gt;The honest position: this checklist plus an automated audit gets you past the failures that cause the most harm per unit of effort. It does not make you accessible, and anyone who tells you a tool does is selling something.&lt;/p&gt;

&lt;p&gt;Automated tools — including ours — catch roughly a third of issues. Run one, then do the keyboard pass yourself.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build your test distribution from your incidents, not from a diagram</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Mon, 19 Oct 2026 16:03:56 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/build-your-test-distribution-from-your-incidents-not-from-a-diagram-2fje</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/build-your-test-distribution-from-your-incidents-not-from-a-diagram-2fje</guid>
      <description>&lt;p&gt;The test pyramid — many unit tests, fewer integration tests, few end-to-end tests — is quoted as though the ratios were derived from something. They were not. It is a heuristic about cost and speed, and it is a reasonable default.&lt;/p&gt;

&lt;p&gt;But it is a default, and most teams never check it against their own evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exercise
&lt;/h2&gt;

&lt;p&gt;Open your last twenty incidents, bugs, or hotfixes. For each one, answer a single question: &lt;strong&gt;what kind of test would have caught this before it shipped?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Categorise honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A unit test — logic inside a function was wrong&lt;/li&gt;
&lt;li&gt;An integration test — two components disagreed about a contract&lt;/li&gt;
&lt;li&gt;An end-to-end test — the behaviour only breaks in a real browser or a real flow&lt;/li&gt;
&lt;li&gt;A contract test — an external service changed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;None of the above&lt;/strong&gt; — a config error, an infrastructure failure, a data problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bucket matters, and it is usually larger than people expect. If a third of your incidents are configuration, no test distribution fixes them and you should be spending that effort on deploy validation instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What teams usually find
&lt;/h2&gt;

&lt;p&gt;Three patterns come up repeatedly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration-heavy team.&lt;/strong&gt; Their bugs are almost all seams — an API returning a shape the frontend did not expect, a migration that broke a query, an enum value nobody handled. They have thousands of unit tests, excellent coverage, and their bugs live in the gaps between the units. The pyramid told them to build more of what was already working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The end-to-end-heavy team.&lt;/strong&gt; Their failures are in real browser behaviour, real devices, real user flows. The slow expensive tests at the top of the pyramid are the ones earning their keep, and they have spent two years feeling guilty about an inverted pyramid that is correct for their product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The config team.&lt;/strong&gt; Half their incidents are not test-shaped at all. Their effort belongs in staged rollouts, health checks, and making bad configuration refuse to boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point is not that the pyramid is wrong
&lt;/h2&gt;

&lt;p&gt;For many teams it is roughly right, and if your evidence produces a pyramid then you have earned it rather than assumed it.&lt;/p&gt;

&lt;p&gt;The point is that the shape should be an output of your failure history, not an input from a diagram drawn about somebody else's system in 2009.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing it continuously
&lt;/h2&gt;

&lt;p&gt;Add one line to your incident template: &lt;em&gt;what kind of test would have caught this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After a year you have a distribution derived from your own product rather than a convention. It costs one line and it is the most useful testing metric I know of — considerably more useful than coverage percentage, which tells you what ran rather than what was checked.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>Writing acceptance criteria a machine can test</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:26:28 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/writing-acceptance-criteria-a-machine-can-test-2jmd</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/writing-acceptance-criteria-a-machine-can-test-2jmd</guid>
      <description>&lt;p&gt;Most acceptance criteria are written for humans who already share context. That is why they cannot be turned into tests without a conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The user can reset their password.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not testable. It does not say what happens to an unknown email, whether the reset link expires, whether an old link works after a second request, or whether the response differs for a registered and unregistered address — which is an account-enumeration decision, not a UX one.&lt;/p&gt;

&lt;p&gt;A developer reads it and picks answers. A tester reads it and picks different answers. The bug report is written months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape that works
&lt;/h2&gt;

&lt;p&gt;Given/When/Then is not ceremony. It forces three things into the open: the starting state, the trigger, and the observable result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; reset link expires after one hour
  &lt;span class="nf"&gt;Given &lt;/span&gt;a password reset was requested 61 minutes ago
  &lt;span class="nf"&gt;When &lt;/span&gt;the user opens the reset link
  &lt;span class="nf"&gt;Then &lt;/span&gt;the page explains the link has expired
  &lt;span class="nf"&gt;And &lt;/span&gt;offers to send a new one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every ambiguity above is now a decision somebody made on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four questions that turn a vague criterion into a testable one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What is true before?&lt;/strong&gt; Not 'the user is on the page' — what state is the data in?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What exactly triggers it?&lt;/strong&gt; A click, a timeout, a webhook, a cron?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What can an outsider observe?&lt;/strong&gt; If the only effect is internal, the test has nothing to assert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the nearest failure?&lt;/strong&gt; Every scenario should have a sibling that fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fourth one catches the most. Teams write the happy path and stop, because the happy path is what the ticket described.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then generate
&lt;/h2&gt;

&lt;p&gt;Once criteria are in this shape, generating the suite is mechanical — which is the point. GoQA takes criteria in exactly this form and returns BDD cases you can export to Gherkin, Playwright, or Cypress. But the value is upstream: criteria specific enough to generate from are criteria specific enough to build from.&lt;/p&gt;

&lt;p&gt;If you take one thing: write the sibling that fails.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>softwaredevelopment</category>
      <category>testing</category>
    </item>
    <item>
      <title>The flaky test that was a tenant-isolation bug</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:26:20 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/the-flaky-test-that-was-a-tenant-isolation-bug-1g4a</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/the-flaky-test-that-was-a-tenant-isolation-bug-1g4a</guid>
      <description>&lt;p&gt;A database test in our suite failed about one run in eight. Here is the full path from symptom to cause, including the three hypotheses that were wrong, because the wrong ones are the useful part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✖ a pending approval is returned with its platform and action type
  AssertionError: 0 !== 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And sometimes, instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  AssertionError: '—' !== 'gmail'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same test, two different failures. That should have been the first clue — one bug producing two shapes usually means the failure is upstream of both assertions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong hypothesis 1: test contention
&lt;/h2&gt;

&lt;p&gt;The obvious read: something else was hammering the database. We had a dashboard running against the same Postgres container.&lt;/p&gt;

&lt;p&gt;Evidence killed it. The tests use a different database, connections were 7 of 100, and query round trips were 0.07s. Three consecutive runs passed with the dashboard up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong hypothesis 2: a CREATE DATABASE race
&lt;/h2&gt;

&lt;p&gt;Each test file drops and recreates its database in &lt;code&gt;before&lt;/code&gt;. Fourteen files run in parallel and Postgres serialises &lt;code&gt;CREATE DATABASE&lt;/code&gt;. Plausible.&lt;/p&gt;

&lt;p&gt;Also wrong — each suite already uses its own database name, so they were never competing for the same one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong hypothesis 3: test ordering
&lt;/h2&gt;

&lt;p&gt;Maybe a later test decided the approval before the read ran. But the tests are sequential — no concurrency flag — and the seed happens in a &lt;code&gt;before&lt;/code&gt; hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual cause
&lt;/h2&gt;

&lt;p&gt;Our repository wraps every tenant-scoped read like this:&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;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;begin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select set_config('app.tenant_id', $1, true)&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;tenantId&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commit&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;The connection is a single long-lived psql session, not a pool. So when two of these overlap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;begin&lt;/span&gt;          &lt;span class="c1"&gt;-- opens A's transaction&lt;/span&gt;
&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;begin&lt;/span&gt;          &lt;span class="c1"&gt;-- no-op, Postgres warns, B is now inside A's transaction&lt;/span&gt;
&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;commit&lt;/span&gt;         &lt;span class="c1"&gt;-- ends the transaction B is still using&lt;/span&gt;
&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;     &lt;span class="c1"&gt;-- runs with no transaction at all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the bug. &lt;code&gt;set_config(..., true)&lt;/code&gt; is &lt;strong&gt;transaction-local&lt;/strong&gt; — which is exactly what you want, because it stops a pooled connection leaking one tenant's context into another request. But when the transaction ends underneath a caller, the setting goes with it. Every row-level security policy then sees an empty tenant and matches nothing.&lt;/p&gt;

&lt;p&gt;Hence &lt;code&gt;0 !== 1&lt;/code&gt;. And when the outer read landed inside a valid context while the joined read did not, &lt;code&gt;'—' !== 'gmail'&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not a flaky test
&lt;/h2&gt;

&lt;p&gt;A retry would have made it green. It would also have shipped a path where one caller's query can execute under another caller's tenant context — which is the isolation boundary the whole system rests on.&lt;/p&gt;

&lt;p&gt;The test was not unreliable. It was intermittently reporting a real defect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Serialise callers per connection, so a transaction cannot be opened inside another:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;WeakMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SqlConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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;predecessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queues&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="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="o"&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="k"&gt;void&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;turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&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;r&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;release&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;queues&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="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;predecessor&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;release&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;Three details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The queue slot is claimed &lt;strong&gt;before&lt;/strong&gt; the first &lt;code&gt;await&lt;/code&gt;, or two callers entering together read the same predecessor and run as one.&lt;/li&gt;
&lt;li&gt;The predecessor's rejection is swallowed. A failed request handed the connection back; inheriting its error would fail an unrelated caller.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;release()&lt;/code&gt; is in a &lt;code&gt;finally&lt;/code&gt;. An unreleased slot stalls every caller behind it — a deadlock is a worse outcome than the bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;Reproduce first. I asserted 'test contention' twice before running a loop that would have told me, and both times the story was plausible enough that I nearly stopped there.&lt;/p&gt;

&lt;p&gt;The loop that found it is six lines, and it asserts the run completed — because a half-killed test run reports as 'clean' and looks exactly like a pass.&lt;/p&gt;

</description>
      <category>database</category>
      <category>debugging</category>
      <category>postgres</category>
      <category>testing</category>
    </item>
    <item>
      <title>Testing time without waiting for it</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Fri, 16 Oct 2026 19:26:10 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/testing-time-without-waiting-for-it-179m</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/testing-time-without-waiting-for-it-179m</guid>
      <description>&lt;p&gt;Anything with an expiry has a first failure that arrives weeks after the deploy. Password reset links, sessions, trial periods, cache entries, retention jobs, token refresh. All of them are testable in milliseconds, and almost none of them are tested at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these go untested
&lt;/h2&gt;

&lt;p&gt;The honest reason is that the obvious test is unbearable:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reset link expires after an hour&lt;/span&gt;&lt;span class="dl"&gt;'&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestReset&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="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;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ...no&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;useLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&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="s1"&gt;expired&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;So it gets skipped, and the expiry logic ships unverified. Which is how you get links that never expire, or expire immediately, or expire correctly but display the wrong message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inject the clock
&lt;/h2&gt;

&lt;p&gt;The fix is to stop reading the current time directly:&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;// Before — untestable&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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;issuedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ONE_HOUR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After — testable, and no less readable&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;issuedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ONE_HOUR&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 the test is instant:&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;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reset link expires after an hour&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&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="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issued&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issued&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;59&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MINUTE&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;false&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="nf"&gt;isExpired&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issued&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issued&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;61&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MINUTE&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;h2&gt;
  
  
  The boundaries that matter
&lt;/h2&gt;

&lt;p&gt;Once time is a parameter, test the edges rather than the middle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exactly at the boundary.&lt;/strong&gt; Is 3600000ms expired or valid? Pick one deliberately; &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;gt;=&lt;/code&gt; are a real decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One unit either side.&lt;/strong&gt; Catches off-by-one in unit conversion, which is the most common bug in this code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative elapsed time.&lt;/strong&gt; Clock skew and daylight-saving transitions both produce it. What does your code do when &lt;code&gt;now&lt;/code&gt; is &lt;em&gt;before&lt;/em&gt; &lt;code&gt;issuedAt&lt;/code&gt;? Usually something wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Far future.&lt;/strong&gt; A token issued and checked a year later should be expired, not overflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The trap: only injecting it halfway
&lt;/h2&gt;

&lt;p&gt;The common failure is injecting the clock into the check but not the &lt;em&gt;creation&lt;/em&gt;. Then your test controls when the link is validated but not when it was issued, and you are back to real time for half the calculation. Both ends need to be parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this generalises
&lt;/h2&gt;

&lt;p&gt;Any non-deterministic input is the same problem: randomness, UUIDs, the filesystem, the network. If a function reads the world directly, it can only be tested in the world. Pass the world in.&lt;/p&gt;

&lt;p&gt;That is the whole of dependency injection, minus the framework.&lt;/p&gt;




&lt;p&gt;If you want the expiry cases enumerated for you, GoQA generates BDD suites from acceptance criteria — including boundary and negative scenarios, which are the ones that get skipped. No sign-up for the demo.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>testing</category>
    </item>
    <item>
      <title>Starting my journey in public: fixing test reliability</title>
      <dc:creator>Goqa Ai</dc:creator>
      <pubDate>Mon, 22 Jun 2026 16:09:59 +0000</pubDate>
      <link>https://dev.to/goqa_ai_c97f712237a7383a5/starting-my-journey-in-public-fixing-test-reliability-183c</link>
      <guid>https://dev.to/goqa_ai_c97f712237a7383a5/starting-my-journey-in-public-fixing-test-reliability-183c</guid>
      <description>&lt;p&gt;Hello everyone, &lt;/p&gt;

&lt;p&gt;I’ve spent years watching teams struggle with something that should be simple: trusting their tests.&lt;/p&gt;

&lt;p&gt;On paper, everything looks fine.&lt;/p&gt;

&lt;p&gt;The test suite is large. Coverage is high. CI is green.&lt;/p&gt;

&lt;p&gt;But in reality?&lt;/p&gt;

&lt;p&gt;Pipelines fail for no clear reason.&lt;/p&gt;

&lt;p&gt;Tests break with small UI changes.&lt;/p&gt;

&lt;p&gt;And teams hesitate when it’s time to deploy.&lt;/p&gt;

&lt;p&gt;Not because they didn’t write enough tests but because maintaining them became the real problem.&lt;/p&gt;

&lt;p&gt;This is something my brother and I have spent a lot of time thinking through and working on together.&lt;/p&gt;

&lt;p&gt;That gap stuck with us.&lt;/p&gt;

&lt;p&gt;So we decided to build something about it.&lt;/p&gt;

&lt;p&gt;I’m a co-founder of GoQA AI, an AI-powered quality engineering platform focused on one thing: making tests reliable again.&lt;/p&gt;

&lt;p&gt;We’re building a system that:&lt;br&gt;
• Generates tests from user stories&lt;br&gt;
• Runs them across web and APIs&lt;br&gt;
• Adapts when the product changes (without constant rewrites)&lt;/p&gt;

&lt;p&gt;No hype. No “AI replaces engineers” narrative.&lt;br&gt;
Just solving a problem most teams quietly deal with: test maintenance.&lt;/p&gt;

&lt;p&gt;We’ve started with web and API testing.&lt;br&gt;
"Mobile is next"&lt;/p&gt;

&lt;p&gt;Over the next few weeks, I’ll be sharing what we’re learning as we build:&lt;br&gt;
→ How self-healing actually works&lt;br&gt;
→ Practical ways to reduce flaky tests&lt;br&gt;
→ Where AI helps and where it doesn’t&lt;br&gt;
→ What building this in public really looks like&lt;/p&gt;

&lt;p&gt;If your team has ever questioned whether failing tests actually mean something…&lt;br&gt;
You’re not alone. &lt;br&gt;
And it’s a problem worth fixing.&lt;/p&gt;

&lt;p&gt;Curious to hear from others here, how do you deal with flaky tests today?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>startup</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
