<?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: 2SD Technologies Limited</title>
    <description>The latest articles on DEV Community by 2SD Technologies Limited (@2sdtechnologiesdotcom).</description>
    <link>https://dev.to/2sdtechnologiesdotcom</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%2F3881189%2Fc5614249-2b9b-4b2f-98e5-e32acfc430e2.png</url>
      <title>DEV Community: 2SD Technologies Limited</title>
      <link>https://dev.to/2sdtechnologiesdotcom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/2sdtechnologiesdotcom"/>
    <language>en</language>
    <item>
      <title>How do you decide two buttons are the same button?</title>
      <dc:creator>2SD Technologies Limited</dc:creator>
      <pubDate>Fri, 11 Sep 2026 08:06:46 +0000</pubDate>
      <link>https://dev.to/2sdtechnologiesdotcom/how-do-you-decide-two-buttons-are-the-same-button-3ip7</link>
      <guid>https://dev.to/2sdtechnologiesdotcom/how-do-you-decide-two-buttons-are-the-same-button-3ip7</guid>
      <description>&lt;p&gt;Every UI test rests on a claim that sounds trivial and is not: &lt;em&gt;this element, on this page today, is the same element I recorded last week.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Selectors are how we usually make that claim, and they make it by proxy. &lt;code&gt;#checkout-btn&lt;/code&gt; does not mean "the button that completes the purchase". It means "whatever has that id", and the relationship between the two is a convention somebody upheld until a release did not.&lt;/p&gt;

&lt;p&gt;So the question worth asking is not how to write more durable selectors. It is what would have to be true for a tool to decide, after a change, that two elements are the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the obvious answers do not survive
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use stable test ids.&lt;/strong&gt; Correct, and it is the best advice in this article -- &lt;code&gt;data-testid&lt;/code&gt; attributes owned by the team, treated as API rather than decoration, will prevent most of this. It is also advice that arrives too late for the suite you already have, and it does not survive a component library upgrade that rewrites the markup underneath your attributes, or a third-party checkout widget you do not control, or the two hundred tests written before the convention existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use XPath from a stable ancestor.&lt;/strong&gt; This trades one fragility for a worse one. A path encodes structure, and structure is the thing most likely to change: one wrapper div for a new layout mode and every path below it is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the accessible name.&lt;/strong&gt; Much better, and genuinely durable, right up to the point where the copy changes. "Continue" becomes "Continue to payment" in a conversion experiment, and a test keyed on visible text is now keyed on a marketing decision.&lt;/p&gt;

&lt;p&gt;Each is right about something. None is right alone, and that is the actual shape of the problem: &lt;strong&gt;element identity is not carried by any single attribute.&lt;/strong&gt; It is distributed across several, each of which is individually unreliable and collectively quite informative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity as a scoring problem
&lt;/h2&gt;

&lt;p&gt;Which suggests treating it the way you would treat any other fuzzy-matching problem: capture several independent signals at record time, and after a change, score candidates against them rather than requiring an exact match on one.&lt;/p&gt;

&lt;p&gt;The signals worth capturing are the ones that tend to change independently of each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Role.&lt;/strong&gt; Is it still a button -- not a link, not a div with a click handler? Role is the most stable of the four, because changing it usually means changing behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label.&lt;/strong&gt; The visible text a person would actually click. Unstable under copy changes, but strong evidence when it matches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position.&lt;/strong&gt; The same place in the same form, expressed relative to its container rather than in pixels. Survives restyling; does not survive reordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neighbours.&lt;/strong&gt; The fields either side of it. This one is underrated: a checkout button whose preceding sibling is still the card-number field is probably still the checkout button, even if its id, text and position all moved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single one of those is worth much. A candidate matching three of four, where the fourth is the one that plausibly changed in this release, is worth a great deal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that decides whether any of this is a good idea
&lt;/h2&gt;

&lt;p&gt;Here is where scoring becomes dangerous rather than clever, and it is worth being blunt about it, because this is the difference between a useful mechanism and a liability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A scorer must be able to decline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the implementation always returns its best candidate, you have built something that converts failures into passes. The test was red because the button genuinely vanished; the scorer finds something button-shaped nearby, clicks it, and the suite goes green. You have not fixed a test. You have removed a signal, and you will not find out until production.&lt;/p&gt;

&lt;p&gt;So the behaviour under uncertainty is the whole design, not an edge case:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A confidence threshold, below which the run fails&lt;/strong&gt; -- and fails as a missing element, with the candidates it considered and their scores, not as a generic assertion error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every accepted match recorded as a change&lt;/strong&gt;, in the run output, with the old identity and the new one side by side. A relocation is an event, not an implementation detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A way to see those events without going looking for them.&lt;/strong&gt; If reviewing relocations is an optional screen somebody has to remember to open, nobody opens it, and you are back to silent passes with extra steps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The uncomfortable implication is that a tool making this claim should sometimes be &lt;em&gt;more&lt;/em&gt; alarming than a brittle one, not less: it should fail loudly in exactly the cases a naive selector would also have failed, and the difference should show up only where the evidence is genuinely strong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask, and what to do if you are not buying anything
&lt;/h2&gt;

&lt;p&gt;If you are evaluating something that advertises self-healing, two questions get to the bottom of it quickly. Ask to see a low-confidence case -- not a demo of a successful relocation, but what happens when the tool is unsure. And ask where accepted relocations appear, and whether anyone on the team has looked at that list this month.&lt;/p&gt;

&lt;p&gt;If you are not buying anything, the useful takeaway is smaller and free: when a test fails on a missing element, the debugging information you want is not "selector not found". It is &lt;em&gt;what else was on the page that nearly matched, and why it was rejected.&lt;/em&gt; Most frameworks will not tell you that, but you can usually capture it yourself in a failure hook -- dump the candidates with the same role, and their labels, before you throw. It turns a class of ten-minute investigations into ten-second ones, and it costs about twenty lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;2SD Technologies builds TAI, a testing platform, and relocation-by-scoring is how it handles interface changes. The reason this article spends more words on declining to match than on matching is that we think it is the only part of the idea that is contentious -- the scoring is straightforward engineering, and the threshold is where a vendor's judgement is either sound or expensive for you.&lt;/p&gt;

&lt;p&gt;Happy to show you the low-confidence case against your own application, if it would be useful: &lt;a href="mailto:info@2sdtechnologies.com"&gt;info@2sdtechnologies.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>devops</category>
      <category>qa</category>
    </item>
    <item>
      <title>Five test tools, five dashboards, and nobody can say what happened</title>
      <dc:creator>2SD Technologies Limited</dc:creator>
      <pubDate>Thu, 10 Sep 2026 12:40:07 +0000</pubDate>
      <link>https://dev.to/2sdtechnologiesdotcom/five-test-tools-five-dashboards-and-nobody-can-say-what-happened-140f</link>
      <guid>https://dev.to/2sdtechnologiesdotcom/five-test-tools-five-dashboards-and-nobody-can-say-what-happened-140f</guid>
      <description>&lt;p&gt;Most teams did not choose to have five testing tools. They accumulated them, one reasonable decision at a time.&lt;/p&gt;

&lt;p&gt;Something was needed for the browser journeys, so a UI framework went in. The API contract broke twice in a quarter, so a contract-testing tool followed. A load test was required before a launch, and the person who ran it picked whatever they knew. Security scanning arrived through the compliance programme rather than through engineering. And somewhere there is a spreadsheet, or a person, comparing generated customer documents against a baseline by eye.&lt;/p&gt;

&lt;p&gt;Each of those was the right call. The sum of them has a property nobody chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bill nobody itemises
&lt;/h2&gt;

&lt;p&gt;Ask what five tools cost and you will be told about licences. That is the smallest line.&lt;/p&gt;

&lt;p&gt;The real cost shows up the first time something fails in a way that spans two of them. Here is the shape it takes, and it is worth walking through slowly, because the difficulty is structural rather than cultural.&lt;/p&gt;

&lt;p&gt;A checkout total comes out wrong in staging on a Thursday afternoon.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;UI suite&lt;/strong&gt; is red on &lt;code&gt;checkout_applies_promotion&lt;/code&gt;. It has a screenshot of a total reading £48.00 where the assertion wanted £43.20.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;API suite&lt;/strong&gt; is green. Every contract test passed, including the one covering the pricing endpoint.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;load run&lt;/strong&gt; was on Tuesday, against a build from Monday, and showed the pricing service at a p99 nobody wrote down, because the run was considered a pass.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;security scan&lt;/strong&gt; is a weekly job. It ran overnight and is unrelated, but it did restart a container.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;document comparison&lt;/strong&gt; is not part of any of this, and the invoice template changed last sprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five signals. One of them is red. And the honest answer to "what happened" is that nobody in the building can say, because no two of those signals can be joined to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why they cannot be joined
&lt;/h2&gt;

&lt;p&gt;This gets mistaken for a communication problem. It is not. The signals are genuinely unjoinable, for four concrete reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no shared run identity.&lt;/strong&gt; The UI suite has a run id. The API suite has a different run id, in a different namespace. Neither carries the other's, and neither carries the id of the build they ran against -- or if it does, it carries the CI job number, which is not the same thing as an artefact version. You cannot ask "show me every signal for build 4471", because no field in any of the five systems means that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no shared clock that matters.&lt;/strong&gt; Every tool timestamps its own events, and the timestamps are accurate. But the UI failure at 14:32:07 and the pricing service's config reload at 14:31:58 live in two systems with no relationship, so the nine seconds between them is a coincidence until a human notices it. Correlating by eyeballing timestamps across browser tabs is the default technique in this situation, and it is about as reliable as it sounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no shared environment of record.&lt;/strong&gt; "Staging" is not one thing. The UI suite pointed at the edge; the API suite pointed at the service directly, bypassing the gateway where the pricing rule actually lives. Both are correct about their own target, and neither is wrong -- which is precisely why two teams can each hold a passing result and still disagree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The artefacts expire on different schedules.&lt;/strong&gt; The screenshot is kept for 30 days by the UI tool's default. The API payloads are in CI logs, gone in 7. The load results are wherever that tool keeps them, possibly on a laptop. By the time the argument is settled, at least one piece of the evidence has aged out.&lt;/p&gt;

&lt;p&gt;So the meeting happens. The UI team says the API changed. The API team says the payload has always looked like that. Somebody suggests re-running everything, which takes until Monday. Nobody is being unhelpful -- they are each holding a true statement about a different system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the release decision is actually made on
&lt;/h2&gt;

&lt;p&gt;Now consider how this reaches a decision.&lt;/p&gt;

&lt;p&gt;Whoever signs off does not look at five dashboards. They look at a summary -- a status in a release ticket, or a message in a channel, written by someone who looked at the dashboards. That person compressed five partially contradictory signals into a sentence.&lt;/p&gt;

&lt;p&gt;So the decision is made on a summary of a summary, by someone with no way to inspect the evidence underneath. The failure mode is not that they decide wrongly. It is that they learn the summary is unreliable, and start deciding on other things instead: whether the team seems confident, whether the last release went fine, whether the date can move.&lt;/p&gt;

&lt;p&gt;That is how a team ends up with a great deal of testing and very little signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "one place" has to mean to be worth anything
&lt;/h2&gt;

&lt;p&gt;The instinct is to consolidate vendors. That is one answer, it is not the only one, and it is worth separating the useful part from the procurement part. The useful part is four properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One run identity that spans every kind of check&lt;/strong&gt;, carried as a field in every event and artefact, and tied to the artefact version rather than the CI job number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One sink.&lt;/strong&gt; Every check writes its result to the same store, in a shape that can be queried across kinds. Not a dashboard embedding five iframes -- one queryable place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One environment of record per run&lt;/strong&gt;, recorded as data, so "the API suite was bypassing the gateway" is a fact you can read rather than something you discover in the meeting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One retention policy across artefacts of the same run&lt;/strong&gt;, so the evidence does not half-expire while it is still under discussion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these requires replacing anything. If you have five tools and no appetite for changing that, the three cheapest moves, in this order, are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generate a run id at the top of the pipeline and pass it into every tool as an environment variable or a tag.&lt;/li&gt;
&lt;li&gt;Ship every tool's structured output to one place, even if that place is a bucket and a table.&lt;/li&gt;
&lt;li&gt;Record the resolved target URL and the artefact version alongside each result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is roughly a day of pipeline work, and it converts the Thursday afternoon above from an argument into a query.&lt;/p&gt;

&lt;p&gt;The reason a single platform is attractive is not that it does something a well-instrumented set of five tools cannot. It is that those four properties come as defaults, rather than as a project somebody has to fund and then maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the signal should land
&lt;/h2&gt;

&lt;p&gt;One more thing, because it tends to be left out of the consolidation conversation.&lt;/p&gt;

&lt;p&gt;Having one queryable place is necessary but not sufficient. If the result of all this is a dashboard, it will be read by the people who already read dashboards. What changes behaviour is the finding arriving where the work happens: an issue in the tracker the developers already have open, carrying the run id, the environment of record, and the artefact.&lt;/p&gt;

&lt;p&gt;Testing is usually described as a stage sitting between development and release. It is more useful to treat it as the signal a release gets judged on -- the thing that has to be legible at the end, to somebody who was not in the room for any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;2SD Technologies builds TAI, a testing platform, so we have an interest in the conclusion. What we would defend is the diagnosis rather than the shopping list: most teams' testing problem is attribution rather than coverage, and the four properties above are worth having whether you get them from one product or from a day of plumbing across the five you already own.&lt;/p&gt;

&lt;p&gt;If you would like to talk it through against your own pipeline, we would be glad to: &lt;a href="mailto:info@2sdtechnologies.com"&gt;info@2sdtechnologies.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>cicd</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your test environment is where the governance stops</title>
      <dc:creator>2SD Technologies Limited</dc:creator>
      <pubDate>Tue, 08 Sep 2026 10:02:53 +0000</pubDate>
      <link>https://dev.to/2sdtechnologiesdotcom/your-test-environment-is-where-the-governance-stops-4978</link>
      <guid>https://dev.to/2sdtechnologiesdotcom/your-test-environment-is-where-the-governance-stops-4978</guid>
      <description>&lt;p&gt;Every estate has a boundary where the controls get serious. Production sits inside it. Access is brokered, changes are reviewed, reads are logged, and somebody owns the log.&lt;/p&gt;

&lt;p&gt;Test environments sit outside it. Not by decision — by accretion. Someone needed a realistic dataset for a migration in 2019, took a snapshot, and the snapshot became the fixture. It got copied into a second environment for load testing, then into a third so a contractor could reproduce a bug. Nobody signed anything, because nobody was doing anything that felt like it needed signing.&lt;/p&gt;

&lt;p&gt;That is how the least-governed part of an estate ends up holding the most production-shaped data in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the data has to be production-shaped
&lt;/h2&gt;

&lt;p&gt;The obvious answer is "then don't use real data", and it is worth being honest about why teams do it anyway.&lt;/p&gt;

&lt;p&gt;Test data has to be representative or the tests are theatre. The interesting defects live in distributional properties: the customer with 4,000 orders, the account whose name contains an apostrophe, the address with no postcode, the record migrated from the system before the system before this one. Synthetic generators produce data that satisfies the schema. Schemas are not where the defects are.&lt;/p&gt;

&lt;p&gt;So teams reach for a subset of production, and the honest ones anonymise it. Anonymisation is real work and it helps, but it is a spectrum rather than a state — a dataset that keeps the distribution keeps a lot of what makes a record identifiable, and a dataset that does not keep the distribution has stopped being useful for the thing you wanted it for.&lt;/p&gt;

&lt;p&gt;The practical position most teams land on is: this data is not production, but it is close enough that we would not want it in a public bucket.&lt;/p&gt;

&lt;p&gt;Then a testing tool is pointed at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that makes a testing tool
&lt;/h2&gt;

&lt;p&gt;A testing tool is an unusual thing to introduce into an estate. Consider what it needs in order to work at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credentials for an application, usually more than one set, usually including a privileged one because half the interesting paths are behind an admin role.&lt;/li&gt;
&lt;li&gt;Network reach into the environment where that application runs.&lt;/li&gt;
&lt;li&gt;The ability to drive the application as a user, which means it can read whatever a user can read.&lt;/li&gt;
&lt;li&gt;Somewhere to keep results — screenshots, request and response payloads, database state before and after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the part people miss when they think about test tooling as a build-time concern. The artefacts a test run produces are a copy of the data the run touched. A failing API assertion is stored with the payload that failed it. A failing UI step is stored with a screenshot of the screen it failed on, and that screen had a customer's name on it.&lt;/p&gt;

&lt;p&gt;So a testing tool is not just a consumer of test data. It is a second store of it, in a place whose retention policy nobody wrote down.&lt;/p&gt;

&lt;p&gt;None of this is an argument against test automation. It is an argument that the access model of a testing tool is not a datasheet appendix. It is the thing that decides whether the tool can be adopted at all in an estate that takes its boundary seriously — and it is why these tools get rejected at review by people who were never against the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four questions, and what a real answer looks like
&lt;/h2&gt;

&lt;p&gt;Reviewers are not asking for reassurance. They are asking for artefacts. Four categories cover most of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity.&lt;/strong&gt; Does the tool authenticate against the directory you already run, or does it keep its own user list? A second user list is not a preference difference — it is a population of accounts that does not get deprovisioned when someone leaves, because your leaver process does not know it exists. The answer a reviewer wants is a protocol, not a promise: SAML or OIDC against the existing identity provider, so joiners and leavers are handled by the process that already handles them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorisation.&lt;/strong&gt; Once someone is in, what can they do? The failure mode here is a tool with one effective permission level, where access control is really URL obscurity — anyone who can reach the run can see the run. Roles need to be coarse enough that people actually use them and fine enough that "can trigger a run against the payments service" and "can read the payloads that run captured" are separable, because those are genuinely different privileges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tenancy.&lt;/strong&gt; If the tool is multi-tenant, what enforces separation, and at what layer? "Separate databases" and "a tenant_id column and a WHERE clause" are both answers to that question, and they are not the same answer. If the tool runs inside your own boundary, the question changes shape rather than disappearing: separation is now your own controls' problem, which is usually what a reviewer prefers, because they can already audit those.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence.&lt;/strong&gt; And this is the one worth being precise about.&lt;/p&gt;

&lt;h2&gt;
  
  
  "A run happened" is not the same artefact as "here is what it could see"
&lt;/h2&gt;

&lt;p&gt;Most tools can produce a list of runs. Who triggered it, when, against which environment, pass or fail. That is an activity log, and it answers a question about the tool.&lt;/p&gt;

&lt;p&gt;The question a reviewer is actually asking is about the data. Which endpoints did that run call? Which records came back? Which of those artefacts are still stored, and for how long? If a screenshot captured a customer record, is that screenshot inside the retention window that applies to the record, or the one that applies to build artefacts?&lt;/p&gt;

&lt;p&gt;Those are different logs. The first is cheap and every tool has it. The second requires the tool to have been built with the assumption that its own outputs are sensitive — which mostly means decisions made early: artefacts stored with the same classification as their source, retention configurable per artefact class rather than globally, and redaction available at capture rather than as a cleanup job.&lt;/p&gt;

&lt;p&gt;You can test for this in about five minutes. Ask to see the audit view for a run that touched customer data, and ask which of the artefacts from that run still exist. A tool that has thought about it will show you. A tool that has not will show you the activity log again.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist
&lt;/h2&gt;

&lt;p&gt;Portable, and not about any particular product:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does it authenticate against our identity provider, or keep its own user list?&lt;/li&gt;
&lt;li&gt;Can "trigger a run" and "read what the run captured" be granted separately?&lt;/li&gt;
&lt;li&gt;What enforces tenant separation, and at which layer?&lt;/li&gt;
&lt;li&gt;For a run that touched customer data: which artefacts still exist, and under whose retention policy?&lt;/li&gt;
&lt;li&gt;Where are those artefacts processed, and does that answer change under load?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fourth one is the one that finds things. It is also the one most likely to be answered with a screenshot of a dashboard rather than a policy, which is itself the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure
&lt;/h2&gt;

&lt;p&gt;2SD Technologies builds TAI, a testing platform, it runs inside the customer's own environment, which means most of question three becomes a question about controls the customer already audits — and the checklist .&lt;/p&gt;

</description>
      <category>testing</category>
      <category>security</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The bug your requirements cannot contain</title>
      <dc:creator>2SD Technologies Limited</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:55:35 +0000</pubDate>
      <link>https://dev.to/2sdtechnologiesdotcom/the-bug-your-requirements-cannot-contain-3gig</link>
      <guid>https://dev.to/2sdtechnologiesdotcom/the-bug-your-requirements-cannot-contain-3gig</guid>
      <description>&lt;p&gt;There is a category of defect that cannot appear in your acceptance criteria. Not because&lt;br&gt;
nobody thought of it, but because the shape of a requirement has no room for it.&lt;/p&gt;

&lt;p&gt;A requirement describes a state and a rule. &lt;em&gt;A customer can apply a valid promo code at&lt;br&gt;
checkout.&lt;/em&gt; State: the code is valid. Rule: it is accepted. Both are evaluated at a single&lt;br&gt;
instant, because a sentence has one tense.&lt;/p&gt;

&lt;p&gt;Real systems do not have one instant. They have two, and sometimes a lot more.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gap between checking and using
&lt;/h2&gt;

&lt;p&gt;Take that promo code. The system validates it when the customer types it into the basket. The&lt;br&gt;
system &lt;em&gt;commits&lt;/em&gt; it when the customer pays. Between those two events sits an unbounded amount&lt;br&gt;
of time — thirty seconds if they have their card handy, three days if they leave the tab open&lt;br&gt;
on a laptop lid.&lt;/p&gt;

&lt;p&gt;If the code expires in that gap, what happens?&lt;/p&gt;

&lt;p&gt;The requirement cannot tell you. It never contemplated a gap, because it was written as one&lt;br&gt;
sentence about one moment. And a test written by hand almost certainly cannot tell you either,&lt;br&gt;
because a person writing a test naturally writes it the way they would perform it: enter code,&lt;br&gt;
assert accepted, pay, assert charged. Three lines, one instant, no gap.&lt;/p&gt;

&lt;p&gt;This is time-of-check to time-of-use. Most developers first meet it as a security problem —&lt;br&gt;
&lt;code&gt;access()&lt;/code&gt; then &lt;code&gt;open()&lt;/code&gt;, and a symlink swapped in between. The same shape appears at business&lt;br&gt;
timescale, and there it is far more common and far less discussed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stock is reserved at basket, decremented at dispatch. Someone else buys the last one.&lt;/li&gt;
&lt;li&gt;A permission is checked when the page loads, enforced when the action fires. The role changed.&lt;/li&gt;
&lt;li&gt;A price is quoted at quote time, charged at renewal. The tariff moved.&lt;/li&gt;
&lt;li&gt;A rate limit is checked at admission, consumed at execution. The window rolled over.&lt;/li&gt;
&lt;li&gt;A feature flag is read at session start, branched on at submit. Someone flipped it.&lt;/li&gt;
&lt;li&gt;A token is validated at the gateway, used by a downstream call. It expired in flight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a real defect class, and not one of them will be written into a&lt;br&gt;
requirement, because in each case the requirement is &lt;em&gt;correct&lt;/em&gt;. It is the world that has two&lt;br&gt;
timestamps.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it survives to production
&lt;/h2&gt;

&lt;p&gt;It survives because it passes every test anybody wrote, and because it is not reproducible on&lt;br&gt;
a developer's machine at a developer's speed. You cannot see it by clicking through the flow,&lt;br&gt;
because you click through the flow in eleven seconds.&lt;/p&gt;

&lt;p&gt;It also survives because the failure is usually &lt;em&gt;silent and plausible&lt;/em&gt;. The customer gets&lt;br&gt;
charged full price and does not notice. The order ships without the discount and support&lt;br&gt;
handles it as a one-off. Nothing goes red. You find out from a pattern in refunds, three&lt;br&gt;
months later, and by then the pattern is the only evidence.&lt;/p&gt;
&lt;h2&gt;
  
  
  Testing for it deliberately
&lt;/h2&gt;

&lt;p&gt;The technique is not complicated. It is just not what a hand-written test looks like, and you&lt;br&gt;
have to decide to do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split the transaction in your test.&lt;/strong&gt; Instead of one continuous flow, make the two moments&lt;br&gt;
explicit and put a mutation between them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# not this
&lt;/span&gt;&lt;span class="nf"&gt;apply_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAVE10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;

&lt;span class="c1"&gt;# this
&lt;/span&gt;&lt;span class="nf"&gt;apply_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAVE10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;basket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="nf"&gt;expire_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SAVE10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# move the world, not the clock in your test runner
&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revalidated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;charge&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;           &lt;span class="c1"&gt;# or whatever you decided it should be
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important line is the third one. To write it you need the ability to change state&lt;br&gt;
&lt;em&gt;between&lt;/em&gt; two calls — which usually means a test seam your system does not have yet, and&lt;br&gt;
which is the real reason this class goes untested. Adding it is the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then pick the case that hurts.&lt;/strong&gt; For each two-phase interaction, ask what changes in the&lt;br&gt;
gap, and who changes it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Who moves the world&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The clock&lt;/td&gt;
&lt;td&gt;the code, token or quote expires&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another user&lt;/td&gt;
&lt;td&gt;the last unit of stock is bought&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An administrator&lt;/td&gt;
&lt;td&gt;a role, a flag or a price is changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The system itself&lt;/td&gt;
&lt;td&gt;a batch job settles or reconciles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four questions per interaction, not thirty. It is a tractable list once you accept it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it in the design
&lt;/h2&gt;

&lt;p&gt;Testing tells you it is broken. Fixing it is a design decision, and there are only really&lt;br&gt;
three options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Re-check at commit.&lt;/strong&gt; The honest default. Costs a round trip and can surprise the user
at the worst moment, which is why it needs a typed result and a real UI state — not an
exception that renders as "something went wrong".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold the promise.&lt;/strong&gt; Reserve the stock, lock the price, honour the code you already
accepted. Correct from the customer's point of view and expensive from the business's, and
it needs an expiry of its own or the reservations pile up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it atomic.&lt;/strong&gt; Collapse the two moments into one so there is no gap. Usually only
available for interactions inside a single system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one option that is never right is the accidental one: check at entry, commit at payment,&lt;br&gt;
and never state which of the three you chose. That is not a design, it is a coin flip&lt;br&gt;
resolved by whoever is unlucky.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable bit
&lt;/h2&gt;

&lt;p&gt;If you have never tested this class, you almost certainly have some. Not because your team is&lt;br&gt;
careless — because a requirement is a sentence, a hand-written test mirrors the requirement,&lt;br&gt;
and neither has a second timestamp in it.&lt;/p&gt;

&lt;p&gt;Pick your most valuable two-phase interaction. Write one test that changes the world in the&lt;br&gt;
middle. See what happens.&lt;/p&gt;




&lt;p&gt;*Disclosure: this post is published by 2SD Technologies, where we build TAI — a testing platform that generates cases from requirements, user stories and the application, including this category. *&lt;/p&gt;

</description>
      <category>testing</category>
      <category>devops</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Generating test cases is the easy part</title>
      <dc:creator>2SD Technologies Limited</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:07:50 +0000</pubDate>
      <link>https://dev.to/2sdtechnologiesdotcom/generating-test-cases-is-the-easy-part-41e9</link>
      <guid>https://dev.to/2sdtechnologiesdotcom/generating-test-cases-is-the-easy-part-41e9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This post has moved.&lt;/strong&gt; The current version, with the animation, is here: &lt;a href="https://dev.to/2sdtechnologiesdotcom/generating-test-cases-is-the-easy-part-2l3a"&gt;https://dev.to/2sdtechnologiesdotcom/generating-test-cases-is-the-easy-part-2l3a&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a ceiling on your test coverage and it is not technical.&lt;/p&gt;

&lt;p&gt;Running tests has been a solved problem for a long time. Runners are fast, parallelism is cheap,&lt;br&gt;
CI is a commodity. None of that touches the actual limit, which is that &lt;strong&gt;somebody has to sit&lt;br&gt;
down, read the requirement, work out what could go wrong, and write the cases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So coverage ends up being a function of how much of that a person had time to do. Not what could&lt;br&gt;
break. Not what changed in this release. What fitted in the sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;Take one line of acceptance criteria:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a customer I can apply a promo code at checkout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask a competent engineer to cover it and you will reliably get three cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a valid code is accepted&lt;/li&gt;
&lt;li&gt;an invalid code is rejected&lt;/li&gt;
&lt;li&gt;an expired code is rejected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three are not a failure of skill. They are the three cases &lt;em&gt;stated in the requirement&lt;/em&gt;.&lt;br&gt;
The requirement has one happy path and two named error conditions, so you get one happy path and&lt;br&gt;
two named error conditions.&lt;/p&gt;

&lt;p&gt;Now here is a different list for the same line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same code applied twice in one basket&lt;/li&gt;
&lt;li&gt;a code at exactly the minimum basket value&lt;/li&gt;
&lt;li&gt;a code combined with a gift card&lt;/li&gt;
&lt;li&gt;a code on a partially refunded order&lt;/li&gt;
&lt;li&gt;a code applied in a second currency&lt;/li&gt;
&lt;li&gt;a code that expires between basket and payment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a defect worth betting on. And not one of them is in the requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Those six are not a grab bag. They fall into categories, and the categories generalise to&lt;br&gt;
anything you are testing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;The question it asks&lt;/th&gt;
&lt;th&gt;In the example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if this happens twice?&lt;/td&gt;
&lt;td&gt;the same code applied twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boundary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What happens exactly at the threshold?&lt;/td&gt;
&lt;td&gt;exactly the minimum basket value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if another feature is active at the same time?&lt;/td&gt;
&lt;td&gt;code plus gift card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prior state&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if the entity has history?&lt;/td&gt;
&lt;td&gt;a partially refunded order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Locale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if the assumptions about format are wrong?&lt;/td&gt;
&lt;td&gt;a second currency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time-of-check to time-of-use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if state changes between validation and commit?&lt;/td&gt;
&lt;td&gt;expires between basket and payment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the interesting one, because it is a genuine race and it is the one least likely&lt;br&gt;
to be written by hand. Validation happens when the code is entered. Commitment happens at&lt;br&gt;
payment. If your expiry check runs only at entry, the bug is invisible in every test that treats&lt;br&gt;
the basket as instantaneous — which is every test somebody writes at their desk.&lt;/p&gt;

&lt;p&gt;The requirement will never mention it. It is not a requirement. It is a consequence of the&lt;br&gt;
requirement meeting a real system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the actual gap.&lt;/strong&gt; Not execution. Not maintenance. Enumeration.&lt;/p&gt;

&lt;h2&gt;
  
  
  So generate them — but that is where it gets interesting
&lt;/h2&gt;

&lt;p&gt;Producing that second list from the requirement plus the code is tractable. Language models are&lt;br&gt;
good at enumerating category × feature combinations, and the categories above are a decent prompt&lt;br&gt;
on their own. You can try it this afternoon.&lt;/p&gt;

&lt;p&gt;The hard part is that a generated case is only worth having if four things are true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It is traceable to something.&lt;/strong&gt; A case you cannot tie back to a clause in the requirement or&lt;br&gt;
a branch in the code is a case nobody can review, and a case nobody reviews will not survive its&lt;br&gt;
first false failure. Whatever generates the case has to carry the provenance with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It is prioritised.&lt;/strong&gt; Six extra cases per acceptance criterion, across a real backlog, is not&lt;br&gt;
coverage — it is a suite nobody runs. Generation without ranking converts an enumeration problem&lt;br&gt;
into a scheduling problem. Something has to say &lt;em&gt;these two matter for this change&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It survives maintenance.&lt;/strong&gt; This is where self-healing selectors earn their place, and it is&lt;br&gt;
worth being precise about why. Self-healing on a hand-written suite is a convenience. Self-healing&lt;br&gt;
on a generated suite is a &lt;strong&gt;precondition&lt;/strong&gt; — because the whole proposition is more tests, and more&lt;br&gt;
tests hand-maintained is just more maintenance. Healing is the floor that makes the volume viable.&lt;br&gt;
It is not the feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. It fails loudly when it is unsure.&lt;/strong&gt; Any mechanism that relocates a moved element is making a&lt;br&gt;
judgement. A tool that quietly picks the nearest match when nothing scores well has not saved you&lt;br&gt;
a failure; it has converted a red test into a green one that proves nothing. &lt;strong&gt;A heal nobody can&lt;br&gt;
review is a silent pass&lt;/strong&gt;, and a silent pass is worse than a broken test because you stop looking.&lt;/p&gt;

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

&lt;p&gt;Generation does not remove human judgement from testing. It moves it — from &lt;em&gt;authoring&lt;/em&gt; cases to&lt;br&gt;
&lt;em&gt;reviewing&lt;/em&gt; them. That is a genuinely better place for it to sit, because reviewing a proposed&lt;br&gt;
case against a requirement is faster than inventing one from scratch, and because the categories&lt;br&gt;
above are easier to check than to remember.&lt;/p&gt;

&lt;p&gt;But it is a trade, not a free win. If your team has no capacity to review, generation will produce&lt;br&gt;
a large suite of plausible tests with unexamined assumptions baked in, and you will have moved the&lt;br&gt;
problem rather than solved it.&lt;/p&gt;

&lt;p&gt;Start with one acceptance criterion. Generate against the six categories. See how many of the&lt;br&gt;
results you would actually have written. That number is your coverage ceiling, and it is usually&lt;br&gt;
uncomfortable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this post is published by 2SD Technologies, where we build TAI — a testing platform that does the generation, prioritisation and healing described above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>devops</category>
      <category>qa</category>
    </item>
    <item>
      <title>Generating test cases is the easy part</title>
      <dc:creator>2SD Technologies Limited</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:20:10 +0000</pubDate>
      <link>https://dev.to/2sdtechnologiesdotcom/generating-test-cases-is-the-easy-part-2l3a</link>
      <guid>https://dev.to/2sdtechnologiesdotcom/generating-test-cases-is-the-easy-part-2l3a</guid>
      <description>&lt;p&gt;There is a ceiling on your test coverage and it is not technical.&lt;/p&gt;

&lt;p&gt;Running tests has been a solved problem for a long time. Runners are fast, parallelism is cheap,&lt;br&gt;
CI is a commodity. None of that touches the actual limit, which is that &lt;strong&gt;somebody has to sit&lt;br&gt;
down, read the requirement, work out what could go wrong, and write the cases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So coverage ends up being a function of how much of that a person had time to do. Not what could&lt;br&gt;
break. Not what changed in this release. What fitted in the sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;Take one line of acceptance criteria:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As a customer I can apply a promo code at checkout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask a competent engineer to cover it and you will reliably get three cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a valid code is accepted&lt;/li&gt;
&lt;li&gt;an invalid code is rejected&lt;/li&gt;
&lt;li&gt;an expired code is rejected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those three are not a failure of skill. They are the three cases &lt;em&gt;stated in the requirement&lt;/em&gt;.&lt;br&gt;
The requirement has one happy path and two named error conditions, so you get one happy path and&lt;br&gt;
two named error conditions.&lt;/p&gt;

&lt;p&gt;Now here is a different list for the same line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the same code applied twice in one basket&lt;/li&gt;
&lt;li&gt;a code at exactly the minimum basket value&lt;/li&gt;
&lt;li&gt;a code combined with a gift card&lt;/li&gt;
&lt;li&gt;a code on a partially refunded order&lt;/li&gt;
&lt;li&gt;a code applied in a second currency&lt;/li&gt;
&lt;li&gt;a code that expires between basket and payment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a defect worth betting on. And not one of them is in the requirement.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcx62mg0iwtg35qd6fy2k.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcx62mg0iwtg35qd6fy2k.gif" alt="One line of acceptance criteria, the three cases a person writes, and the six a requirement never states" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Three to nine, from the same one line.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Those six are not a grab bag. They fall into categories, and the categories generalise to&lt;br&gt;
anything you are testing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;The question it asks&lt;/th&gt;
&lt;th&gt;In the example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if this happens twice?&lt;/td&gt;
&lt;td&gt;the same code applied twice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boundary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What happens exactly at the threshold?&lt;/td&gt;
&lt;td&gt;exactly the minimum basket value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if another feature is active at the same time?&lt;/td&gt;
&lt;td&gt;code plus gift card&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prior state&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if the entity has history?&lt;/td&gt;
&lt;td&gt;a partially refunded order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Locale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if the assumptions about format are wrong?&lt;/td&gt;
&lt;td&gt;a second currency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time-of-check to time-of-use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What if state changes between validation and commit?&lt;/td&gt;
&lt;td&gt;expires between basket and payment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the interesting one, because it is a genuine race and it is the one least likely&lt;br&gt;
to be written by hand. Validation happens when the code is entered. Commitment happens at&lt;br&gt;
payment. If your expiry check runs only at entry, the bug is invisible in every test that treats&lt;br&gt;
the basket as instantaneous — which is every test somebody writes at their desk.&lt;/p&gt;

&lt;p&gt;The requirement will never mention it. It is not a requirement. It is a consequence of the&lt;br&gt;
requirement meeting a real system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the actual gap.&lt;/strong&gt; Not execution. Not maintenance. Enumeration.&lt;/p&gt;

&lt;h2&gt;
  
  
  So generate them — but that is where it gets interesting
&lt;/h2&gt;

&lt;p&gt;Producing that second list from the requirement plus the code is tractable. Language models are&lt;br&gt;
good at enumerating category × feature combinations, and the categories above are a decent prompt&lt;br&gt;
on their own. You can try it this afternoon.&lt;/p&gt;

&lt;p&gt;The hard part is that a generated case is only worth having if four things are true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It is traceable to something.&lt;/strong&gt; A case you cannot tie back to a clause in the requirement or&lt;br&gt;
a branch in the code is a case nobody can review, and a case nobody reviews will not survive its&lt;br&gt;
first false failure. Whatever generates the case has to carry the provenance with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It is prioritised.&lt;/strong&gt; Six extra cases per acceptance criterion, across a real backlog, is not&lt;br&gt;
coverage — it is a suite nobody runs. Generation without ranking converts an enumeration problem&lt;br&gt;
into a scheduling problem. Something has to say &lt;em&gt;these two matter for this change&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It survives maintenance.&lt;/strong&gt; This is where self-healing selectors earn their place, and it is&lt;br&gt;
worth being precise about why. Self-healing on a hand-written suite is a convenience. Self-healing&lt;br&gt;
on a generated suite is a &lt;strong&gt;precondition&lt;/strong&gt; — because the whole proposition is more tests, and more&lt;br&gt;
tests hand-maintained is just more maintenance. Healing is the floor that makes the volume viable.&lt;br&gt;
It is not the feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. It fails loudly when it is unsure.&lt;/strong&gt; Any mechanism that relocates a moved element is making a&lt;br&gt;
judgement. A tool that quietly picks the nearest match when nothing scores well has not saved you&lt;br&gt;
a failure; it has converted a red test into a green one that proves nothing. &lt;strong&gt;A heal nobody can&lt;br&gt;
review is a silent pass&lt;/strong&gt;, and a silent pass is worse than a broken test because you stop looking.&lt;/p&gt;

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

&lt;p&gt;Generation does not remove human judgement from testing. It moves it — from &lt;em&gt;authoring&lt;/em&gt; cases to&lt;br&gt;
&lt;em&gt;reviewing&lt;/em&gt; them. That is a genuinely better place for it to sit, because reviewing a proposed&lt;br&gt;
case against a requirement is faster than inventing one from scratch, and because the categories&lt;br&gt;
above are easier to check than to remember.&lt;/p&gt;

&lt;p&gt;But it is a trade, not a free win. If your team has no capacity to review, generation will produce&lt;br&gt;
a large suite of plausible tests with unexamined assumptions baked in, and you will have moved the&lt;br&gt;
problem rather than solved it.&lt;/p&gt;

&lt;p&gt;Start with one acceptance criterion. Generate against the six categories. See how many of the&lt;br&gt;
results you would actually have written. That number is your coverage ceiling, and it is usually&lt;br&gt;
uncomfortable.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this post is published by 2SD Technologies, where we build TAI — a testing platform that does the generation, prioritisation and healing described above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>ai</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
