<?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>Forem: idavidov13</title>
    <description>The latest articles on Forem by idavidov13 (@idavidov13).</description>
    <link>https://forem.com/idavidov13</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2895129%2F968e3111-e348-4f29-a8ad-6daf273a65b9.jpeg</url>
      <title>Forem: idavidov13</title>
      <link>https://forem.com/idavidov13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/idavidov13"/>
    <language>en</language>
    <item>
      <title>REST API Testing: What Every QA Engineer Must Know</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Thu, 16 Apr 2026 12:18:51 +0000</pubDate>
      <link>https://forem.com/idavidov13/rest-api-testing-what-every-qa-engineer-must-know-4df0</link>
      <guid>https://forem.com/idavidov13/rest-api-testing-what-every-qa-engineer-must-know-4df0</guid>
      <description>&lt;p&gt;Imagine this: you test a POST endpoint that creates a new user. It returns &lt;code&gt;201 Created&lt;/code&gt;. You mark the test as passed and move on. Two weeks later, production breaks - the response was missing a required field, a price came back as a string instead of a number, and nobody validated the response schema. Sound familiar?&lt;/p&gt;

&lt;p&gt;REST API testing goes far beyond "send a request, check the status code". A solid API test verifies the method, the parameters, the payload, the status code, the response body, &lt;strong&gt;and&lt;/strong&gt; the response structure. Miss any one of those, and bugs slip through.&lt;/p&gt;

&lt;p&gt;This guide covers everything you need to test REST APIs thoroughly - from HTTP methods and payloads to status codes and schema validation. No tool-specific code, no fluff. Just the engineering fundamentals that apply everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧭 What Is a REST API
&lt;/h2&gt;

&lt;p&gt;Think of a REST API as a restaurant's ordering system. You (the client) send an order (a request) to the kitchen (the server). The kitchen processes it and sends back your food (the response). The menu defines what you can order (the endpoints), and the waiter gives you feedback on how it went (the status code).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; (Representational State Transfer) is an architectural style where clients communicate with servers over HTTP. Every "thing" in the system - a user, an order, a product - is a &lt;strong&gt;resource&lt;/strong&gt;, identified by a URL. You interact with resources using standard HTTP methods, and data travels back and forth as JSON.&lt;/p&gt;

&lt;p&gt;That's it. Client sends a request, server sends a response. Everything we test revolves around this exchange.&lt;/p&gt;




&lt;h2&gt;
  
  
  📬 HTTP Methods - The Five Actions
&lt;/h2&gt;

&lt;p&gt;Every API request starts with a method. The method tells the server &lt;strong&gt;what you want to do&lt;/strong&gt; with a resource. There are five you'll use constantly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Idempotent&lt;/th&gt;
&lt;th&gt;Has Request Body&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Retrieve data&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Create a new resource&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;Replace an entire resource&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;Partially update a resource&lt;/td&gt;
&lt;td&gt;Usually*&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;Remove a resource&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Idempotent&lt;/strong&gt; means calling the same request multiple times produces the same result. A GET request for user #42 always returns user #42. A POST that creates an order will create a new one every time - that's why it's not idempotent.&lt;/p&gt;

&lt;p&gt;*PATCH is typically idempotent, but not guaranteed. An operation like &lt;code&gt;{"increment": 1}&lt;/code&gt; would add 1 every time you call it - that's non-idempotent. Always verify the specific API's behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcmy5q7zueyxamcv2e3oe.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcmy5q7zueyxamcv2e3oe.webp" alt="The five HTTP methods as distinct actions" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GET&lt;/strong&gt; - Retrieves a resource without changing anything on the server. Think of it as reading a menu without placing an order.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/users/42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response body (&lt;code&gt;200 OK&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane.smith@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"developer"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;POST&lt;/strong&gt; - Creates a new resource. You send the data, and the server assigns an ID and stores it.&lt;/p&gt;

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

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

&lt;/div&gt;



&lt;p&gt;Request payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.doe@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tester"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response body (&lt;code&gt;201 Created&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.doe@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tester"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-16T10:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;PUT vs PATCH&lt;/strong&gt; - This is where most confusion happens. &lt;strong&gt;PUT replaces the entire resource.&lt;/strong&gt; If you send a PUT without a required field, the API will most likely reject it with a &lt;code&gt;400 Bad Request&lt;/code&gt;. &lt;strong&gt;PATCH updates only the fields you include.&lt;/strong&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PUT /api/users/42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request payload (full resource replacement):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane.updated@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"developer"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every field must be present. Miss a required field like &lt;code&gt;role&lt;/code&gt;, and the API will either reject the request with &lt;code&gt;400 Bad Request&lt;/code&gt; or set the value to &lt;code&gt;null&lt;/code&gt; - depending on the implementation. Both behaviors are worth testing.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PATCH /api/users/42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request payload (partial update):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane.updated@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the email changes. Everything else stays untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DELETE&lt;/strong&gt; - Removes a resource. Usually returns &lt;code&gt;204 No Content&lt;/code&gt; with an empty body.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;DELETE /api/users/42
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response: &lt;code&gt;204 No Content&lt;/code&gt; (empty body).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to test for each method:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Happy path&lt;/strong&gt; - Does the server return the correct success status code? (&lt;code&gt;200&lt;/code&gt; for GET, PUT, and PATCH, &lt;code&gt;201&lt;/code&gt; for POST, &lt;code&gt;204&lt;/code&gt; for DELETE)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side effects&lt;/strong&gt; - Did the operation actually happen? After a POST, can you GET the new resource? After a DELETE, does a GET return &lt;code&gt;404&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response body&lt;/strong&gt; - Does it contain all expected fields with correct values and types?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing or invalid data&lt;/strong&gt; - Does a POST with a missing required field return &lt;code&gt;400&lt;/code&gt;? Does a PUT with the wrong data type return &lt;code&gt;400&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-existent resources&lt;/strong&gt; - Does a GET, PUT, PATCH, or DELETE for an ID that doesn't exist return &lt;code&gt;404&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication and authorization&lt;/strong&gt; - Does the endpoint return &lt;code&gt;401&lt;/code&gt; without a token and &lt;code&gt;403&lt;/code&gt; with insufficient permissions?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate/conflict&lt;/strong&gt; - Does a POST with data that already exists (e.g., a duplicate email) return &lt;code&gt;409 Conflict&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method not supported&lt;/strong&gt; - Does sending a DELETE to a read-only endpoint return &lt;code&gt;405&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response headers&lt;/strong&gt; - Does every response return &lt;code&gt;Content-Type: application/json&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📨 Request Headers - The Metadata Layer
&lt;/h2&gt;

&lt;p&gt;Headers are the metadata of your request. They travel alongside the URL and body, telling the server how to interpret the request, who you are, and what format you expect back.&lt;/p&gt;

&lt;p&gt;Think of headers like the shipping label on a package. The package contents are your payload, but the label tells the courier where it's going, how fragile it is, and who sent it. Without the right label, even a perfect package won't reach its destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headers every API tester should know:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Example Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Authorization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identifies who you are&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bearer eyJhbGciOiJIUzI1NiIs...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Content-Type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tells the server what format you're sending&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Accept&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tells the server what format you want back&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X-Request-ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Traces a request through distributed systems&lt;/td&gt;
&lt;td&gt;&lt;code&gt;req-abc-123-def-456&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controls caching behavior&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Authorization patterns:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most APIs use one of these authentication methods:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bearer tokens&lt;/strong&gt; - The most common pattern. You send a token in the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;API keys&lt;/strong&gt; - Simpler but less secure. Often sent as a header or query parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/products
X-API-Key: sk_live_abc123def456
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to test for headers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing Authorization&lt;/strong&gt; - Does the API return &lt;code&gt;401 Unauthorized&lt;/code&gt; when no token is provided?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid token&lt;/strong&gt; - Does an expired, malformed, or revoked token return &lt;code&gt;401&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insufficient permissions&lt;/strong&gt; - Does a valid token with wrong role return &lt;code&gt;403 Forbidden&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong Content-Type&lt;/strong&gt; - What happens when you send JSON but declare &lt;code&gt;Content-Type: text/plain&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Content-Type&lt;/strong&gt; - Does the API assume JSON, reject the request, or crash?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept header mismatch&lt;/strong&gt; - If you request &lt;code&gt;Accept: application/xml&lt;/code&gt; but the API only supports JSON, what happens?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 Query Parameters - Filtering, Sorting, Paging
&lt;/h2&gt;

&lt;p&gt;Query parameters live in the URL after a &lt;code&gt;?&lt;/code&gt; sign. They don't change the resource - they change &lt;strong&gt;how&lt;/strong&gt; you retrieve it. Think of them as filters in an online store: "Show me electronics, sorted by price, cheapest first, second page, 20 items per page."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/products?category=electronics&amp;amp;sort=price&amp;amp;order=asc&amp;amp;page=2&amp;amp;limit=20
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This request says: "Give me electronics, sorted by price ascending, second page, 20 items per page."&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wireless Mouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USB-C Hub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;34.99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pagination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasNext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasPrevious"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When testing pagination, verify all these metadata fields - not just the items in &lt;code&gt;data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Common query parameter patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filtering&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?status=active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return only matching items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sorting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?sort=createdAt&amp;amp;order=desc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Control result order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?page=1&amp;amp;limit=25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return given page and Limit results per it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Searching&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?q=wireless+headphones&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full-text search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field selection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;?fields=id,name,price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return only specific fields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What to test:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missing parameters&lt;/strong&gt; - What happens when you omit &lt;code&gt;page&lt;/code&gt; or &lt;code&gt;limit&lt;/code&gt;? The API should use sensible defaults, not crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalid values&lt;/strong&gt; - &lt;code&gt;?page=-1&lt;/code&gt;, &lt;code&gt;?limit=abc&lt;/code&gt;, &lt;code&gt;?sort=nonExistentField&lt;/code&gt;. Expect a &lt;code&gt;400 Bad Request&lt;/code&gt;, not a &lt;code&gt;500&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundary values&lt;/strong&gt; - &lt;code&gt;?limit=0&lt;/code&gt;, &lt;code&gt;?limit=10000&lt;/code&gt;, &lt;code&gt;?page=999999&lt;/code&gt;. Does the API handle extremes gracefully?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty results&lt;/strong&gt; - A valid filter that matches nothing should return an empty array with &lt;code&gt;200&lt;/code&gt;, not a &lt;code&gt;404&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combinations&lt;/strong&gt; - Test filters together. Does &lt;code&gt;?category=electronics&amp;amp;status=active&lt;/code&gt; return items that match &lt;strong&gt;both&lt;/strong&gt; conditions?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📦 Request Payloads - What You Send Matters
&lt;/h2&gt;

&lt;p&gt;The request payload (or body) is the data you send with POST, PUT, and PATCH requests. It's typically JSON, and it needs a &lt;code&gt;Content-Type: application/json&lt;/code&gt; header so the server knows how to parse it.&lt;/p&gt;

&lt;p&gt;A payload has a defined structure. Each field has a name, a data type, and constraints (required or optional, minimum/maximum length, allowed values). Here's a typical one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wireless Headphones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Noise-cancelling over-ear headphones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;149.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electronics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inStock"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"wireless"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noise-cancelling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bluetooth"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"specifications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"250g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"batteryLife"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30 hours"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"connectivity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bluetooth 5.3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the variety: strings, numbers, booleans, arrays, and objects. Each type needs different testing strategies. And that's exactly what the next section is about.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Testing Payload Parameters Like a QA Engineer
&lt;/h2&gt;

&lt;p&gt;Sending a valid payload is the easy part. The real value of API testing comes from answering: &lt;strong&gt;what happens when the payload is wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A systematic approach tests each field across multiple dimensions. Here's the framework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Required vs Optional Fields&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For every required field, test what happens when it's missing from the payload entirely. The API should return &lt;code&gt;400 Bad Request&lt;/code&gt; with a clear error message - not silently accept incomplete data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing the required 'name' field"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electronics"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;400 Bad Request&lt;/code&gt; with a message like &lt;code&gt;"name is required"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Type Violations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Send the wrong type for each field. A price should be a number - what happens when you send a string?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Expected Type&lt;/th&gt;
&lt;th&gt;Test With&lt;/th&gt;
&lt;th&gt;Expected Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;price&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"free"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 Bad Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inStock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"yes"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 Bad Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;array&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"single-tag"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 Bad Request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;12345&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 Bad Request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Boundary Values&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every field with constraints has edges worth testing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test Case&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Expected Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Empty string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"name": ""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 (if name is required)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimum length&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"name": "A"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Depends on min constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum length&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"name": "A" × 256 chars&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 if exceeds max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negative number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"price": -10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 (prices can't be negative)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"price": 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Depends on business rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Very large number&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"price": 999999999.99&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Depends on max constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Null, Empty, and Missing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These three are different, and many bugs hide in the distinction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A field set to &lt;code&gt;null&lt;/code&gt;, a field set to an empty string, and a field that's absent entirely may all be handled differently by the API. Test all three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special Characters and Injection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when you send &lt;code&gt;&amp;lt;script&amp;gt;alert('xss')&amp;lt;/script&amp;gt;&lt;/code&gt; as a name? The API should sanitize or reject these inputs, never execute them.&lt;/p&gt;

&lt;p&gt;A secure API handles this in one of two ways.&lt;/p&gt;

&lt;p&gt;Request payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;script&amp;gt;alert('xss')&amp;lt;/script&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response (rejection approach) - &lt;code&gt;400 Bad Request&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INVALID_INPUT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name contains invalid characters"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response (sanitization approach) - &lt;code&gt;201 Created&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;amp;lt;script&amp;amp;gt;alert('xss')&amp;amp;lt;/script&amp;amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either response is acceptable. What's never acceptable is storing and returning the raw malicious input unchanged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Field Interaction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some fields depend on each other. If &lt;code&gt;currency&lt;/code&gt; is required when &lt;code&gt;price&lt;/code&gt; is present, test sending &lt;code&gt;price&lt;/code&gt; without &lt;code&gt;currency&lt;/code&gt;. If &lt;code&gt;endDate&lt;/code&gt; must be after &lt;code&gt;startDate&lt;/code&gt;, test sending them in reverse order.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The goal isn't to break the API for fun. It's to verify that the API protects itself - and its data - from every kind of invalid input.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🚦 Status Codes - The API's Way of Talking Back
&lt;/h2&gt;

&lt;p&gt;If the request is what you say to the API, the status code is its body language. It tells you immediately whether things went well, whether you messed up, or whether the server had a problem.&lt;/p&gt;

&lt;p&gt;Status codes are grouped into three families that matter for testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2xx - "Everything went well"&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;When It's Returned&lt;/th&gt;
&lt;th&gt;What to Verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;Successful GET, PUT, PATCH&lt;/td&gt;
&lt;td&gt;Response body contains the expected data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;Created&lt;/td&gt;
&lt;td&gt;Successful POST&lt;/td&gt;
&lt;td&gt;Response body contains the new resource with a server-assigned &lt;code&gt;id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;td&gt;Accepted&lt;/td&gt;
&lt;td&gt;Async operations (background jobs, batch processing)&lt;/td&gt;
&lt;td&gt;Response confirms the request was accepted. May include a job ID or status URL for polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;204&lt;/td&gt;
&lt;td&gt;No Content&lt;/td&gt;
&lt;td&gt;Successful DELETE, or updates that return no body&lt;/td&gt;
&lt;td&gt;Response body is &lt;strong&gt;empty&lt;/strong&gt;. Don't try to parse it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference between &lt;code&gt;200&lt;/code&gt; and &lt;code&gt;201&lt;/code&gt; matters. A POST that returns &lt;code&gt;200&lt;/code&gt; instead of &lt;code&gt;201&lt;/code&gt; is technically working, but it's not following REST conventions - and that's a valid bug to report.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;204&lt;/code&gt; is the quiet confirmation. The API did what you asked but has nothing to say about it. After a DELETE, this is exactly what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4xx - "You made a mistake"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are client errors. The request was wrong in some way, and the server is telling you what happened.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Common Cause&lt;/th&gt;
&lt;th&gt;Example Scenario&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;Bad Request&lt;/td&gt;
&lt;td&gt;Malformed JSON, wrong data types, missing required fields&lt;/td&gt;
&lt;td&gt;Sending &lt;code&gt;"price": "abc"&lt;/code&gt; instead of a number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;Unauthorized&lt;/td&gt;
&lt;td&gt;Missing or invalid authentication token&lt;/td&gt;
&lt;td&gt;Calling an endpoint without an &lt;code&gt;Authorization&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;Forbidden&lt;/td&gt;
&lt;td&gt;Valid auth, but insufficient permissions&lt;/td&gt;
&lt;td&gt;A regular user trying to access an admin-only endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;Not Found&lt;/td&gt;
&lt;td&gt;Resource doesn't exist, or wrong URL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /api/users/99999&lt;/code&gt; when that user doesn't exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;Method Not Allowed&lt;/td&gt;
&lt;td&gt;Using the wrong HTTP method&lt;/td&gt;
&lt;td&gt;Sending a &lt;code&gt;DELETE&lt;/code&gt; to an endpoint that only supports &lt;code&gt;GET&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;td&gt;Conflict&lt;/td&gt;
&lt;td&gt;Resource state conflict&lt;/td&gt;
&lt;td&gt;Creating a user with an email that already exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;Too Many Requests&lt;/td&gt;
&lt;td&gt;Rate limit exceeded&lt;/td&gt;
&lt;td&gt;Sending 100 requests per second to an endpoint limited to 10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The distinction between &lt;code&gt;401&lt;/code&gt; and &lt;code&gt;403&lt;/code&gt; is critical and often confused. &lt;strong&gt;401 means "I don't know who you are"&lt;/strong&gt; - provide credentials. &lt;strong&gt;403 means "I know who you are, but you're not allowed to do this"&lt;/strong&gt; - different credentials won't help unless you have a higher permission level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a good error response looks like:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A well-designed API returns structured error responses that help you understand exactly what went wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VALIDATION_ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Request validation failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Must be a positive number"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When testing errors, verify the response includes all three levels: a machine-readable code, a human-readable message, and field-level details when applicable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to test for 4xx codes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify the response includes a meaningful error message, not just the status code&lt;/li&gt;
&lt;li&gt;Check that error messages don't leak sensitive information (stack traces, database details, internal paths)&lt;/li&gt;
&lt;li&gt;Confirm the server didn't partially process the request. A &lt;code&gt;400&lt;/code&gt; on a creation endpoint should mean nothing was created&lt;/li&gt;
&lt;li&gt;Verify field-level errors point to the correct field name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5xx - "The server broke"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are server-side errors. The client did nothing wrong, but the server couldn't fulfill the request.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What It Means&lt;/th&gt;
&lt;th&gt;Testing Angle&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;Internal Server Error&lt;/td&gt;
&lt;td&gt;Unhandled exception on the server&lt;/td&gt;
&lt;td&gt;This should never happen with valid input. If it does, that's a bug - report it with the exact request that triggered it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;502&lt;/td&gt;
&lt;td&gt;Bad Gateway&lt;/td&gt;
&lt;td&gt;The server received an invalid response from an upstream service&lt;/td&gt;
&lt;td&gt;Common in microservice architectures. Test when a dependency is down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;503&lt;/td&gt;
&lt;td&gt;Service Unavailable&lt;/td&gt;
&lt;td&gt;Server is overloaded or under maintenance&lt;/td&gt;
&lt;td&gt;May include a &lt;code&gt;Retry-After&lt;/code&gt; header. Verify the API returns this during deployments or heavy load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;Gateway Timeout&lt;/td&gt;
&lt;td&gt;An upstream service took too long to respond&lt;/td&gt;
&lt;td&gt;Similar to 502 but specifically about timing. Test with slow-responding dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The golden rule for 5xx codes:&lt;/strong&gt; a properly built API should never return &lt;code&gt;500&lt;/code&gt; in response to any client input, no matter how bizarre. If you can trigger a &lt;code&gt;500&lt;/code&gt; by sending unexpected data, that's a bug in the API's error handling. The server should catch internal errors and return an appropriate &lt;code&gt;4xx&lt;/code&gt; with a helpful message.&lt;/p&gt;

&lt;p&gt;Here's a quick mental model for the full picture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;Who's at fault?&lt;/th&gt;
&lt;th&gt;Your reaction as a tester&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2xx&lt;/td&gt;
&lt;td&gt;Nobody - it worked&lt;/td&gt;
&lt;td&gt;Verify the response body and schema&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4xx&lt;/td&gt;
&lt;td&gt;The client&lt;/td&gt;
&lt;td&gt;Verify error messages are clear and no data was modified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5xx&lt;/td&gt;
&lt;td&gt;The server&lt;/td&gt;
&lt;td&gt;Report it - this is always a bug&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzbpukid0pqiknn0edt4n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzbpukid0pqiknn0edt4n.webp" alt="Status code families as traffic signals" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ Schema Validation - The Safety Net You're Probably Missing
&lt;/h2&gt;

&lt;p&gt;You've checked the status code. You've checked a few fields in the response. But have you checked the &lt;strong&gt;entire structure&lt;/strong&gt; of the response?&lt;/p&gt;

&lt;p&gt;Schema validation is like checking your order at a restaurant. The waiter brings your plate and smiles - that's your &lt;code&gt;200 OK&lt;/code&gt;. But do you just trust the smile?&lt;/p&gt;

&lt;p&gt;You check that every dish you ordered is actually there, that the steak is steak and not chicken, that the side is fries and not salad, and that nothing extra ended up on the plate. Schema validation does exactly this for API responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why status code alone isn't enough:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;200 OK&lt;/code&gt; response that looks like this is a problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane Smith"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"developer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"85000"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status code says "success," but &lt;code&gt;email&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; when it shouldn't be, and &lt;code&gt;salary&lt;/code&gt; is a string instead of a number. Without schema validation, your test passes. With it, you catch the issue immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to validate in a response schema:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What It Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Field presence&lt;/td&gt;
&lt;td&gt;Missing fields that should always be there&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data types&lt;/td&gt;
&lt;td&gt;A number returned as a string, a boolean as &lt;code&gt;"true"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required vs optional&lt;/td&gt;
&lt;td&gt;A required field came back as &lt;code&gt;null&lt;/code&gt; or was absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Array structure&lt;/td&gt;
&lt;td&gt;An array that should contain objects contains strings instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested objects&lt;/td&gt;
&lt;td&gt;A nested &lt;code&gt;address&lt;/code&gt; object is missing the &lt;code&gt;zipCode&lt;/code&gt; field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enum values&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;status&lt;/code&gt; field returns &lt;code&gt;"actve"&lt;/code&gt; (typo) instead of &lt;code&gt;"active"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;email&lt;/code&gt; field contains &lt;code&gt;"not-an-email"&lt;/code&gt;, a &lt;code&gt;date&lt;/code&gt; field contains &lt;code&gt;"yesterday"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;JSON Schema in practice:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JSON Schema is a standard that lets you define exactly what a valid response looks like. Here's a schema for a user response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"developer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tester"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"manager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date-time"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema enforces that &lt;code&gt;id&lt;/code&gt; is a positive integer, &lt;code&gt;name&lt;/code&gt; is a non-empty string, &lt;code&gt;email&lt;/code&gt; follows an email format, &lt;code&gt;role&lt;/code&gt; is one of four allowed values, and no unexpected fields are present. If the API returns anything that doesn't match, the validation fails - and your test catches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When schema validation saves you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A developer adds a new field to the response but forgets to update the documentation. &lt;code&gt;additionalProperties: false&lt;/code&gt; catches it.&lt;/li&gt;
&lt;li&gt;A database migration changes a column type from integer to string. Your schema says &lt;code&gt;"type": "integer"&lt;/code&gt; - caught.&lt;/li&gt;
&lt;li&gt;A null value sneaks in because of a missing database join. The &lt;code&gt;required&lt;/code&gt; array catches it.&lt;/li&gt;
&lt;li&gt;An enum value is misspelled. The &lt;code&gt;enum&lt;/code&gt; constraint catches it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vyd364nac5xn7uuz64f.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vyd364nac5xn7uuz64f.webp" alt="Schema validation as the safety net" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Schema validation doesn't replace field-level assertions. It complements them. Check the schema for structure, then assert specific values for business logic.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🗺️ Putting It All Together - The API Test Checklist
&lt;/h2&gt;

&lt;p&gt;Here's the practical checklist you can use for every endpoint. Think of it as the minimum bar for thorough API test coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For every endpoint, verify:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Method behavior&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the endpoint respond to the correct HTTP method?&lt;/li&gt;
&lt;li&gt;Does it return &lt;code&gt;405 Method Not Allowed&lt;/code&gt; for unsupported methods?&lt;/li&gt;
&lt;li&gt;Is the operation idempotent where it should be?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Query parameters (GET endpoints)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do filters return only matching results?&lt;/li&gt;
&lt;li&gt;Do defaults apply when parameters are omitted?&lt;/li&gt;
&lt;li&gt;Are invalid parameter values rejected with &lt;code&gt;400&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Does pagination work correctly at boundaries (first page, last page, beyond last page)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Request payload (POST/PUT/PATCH)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are required fields enforced?&lt;/li&gt;
&lt;li&gt;Are data types validated?&lt;/li&gt;
&lt;li&gt;Are boundary values handled correctly?&lt;/li&gt;
&lt;li&gt;Is null/empty/missing treated appropriately for each field?&lt;/li&gt;
&lt;li&gt;Are field dependencies enforced?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Status codes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the happy path return the correct 2xx code (not just "any 200")?&lt;/li&gt;
&lt;li&gt;Does invalid input return the correct 4xx code with a clear error message?&lt;/li&gt;
&lt;li&gt;Can you trigger a 5xx with any input? (If yes, report it - that's a bug)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. Response body&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the response contain all expected fields?&lt;/li&gt;
&lt;li&gt;Are field values correct (not just present)?&lt;/li&gt;
&lt;li&gt;Are related resources updated? (After POST, does GET return the new item?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. Schema validation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the response match the defined JSON Schema?&lt;/li&gt;
&lt;li&gt;Are data types correct for every field?&lt;/li&gt;
&lt;li&gt;Are required fields always present and non-null?&lt;/li&gt;
&lt;li&gt;Are enum values within the allowed set?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67xbxupn150f9izkfxy0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67xbxupn150f9izkfxy0.webp" alt="The testing checklist as layers of verification" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This checklist applies regardless of your tool - whether you're using Postman, curl, Playwright, REST Assured, or any other framework. The principles are universal.&lt;/p&gt;

&lt;p&gt;Ready to put this into practice? &lt;a href="https://idavidov.eu/building-playwright-framework-step-by-step-implementing-api-tests" rel="noopener noreferrer"&gt;Implementing API Tests&lt;/a&gt; shows you how to build these concepts into a real Playwright test suite with fixtures and schema validation.&lt;/p&gt;

&lt;p&gt;For a structured path through more QA engineering fundamentals, the &lt;a href="https://idavidov.eu/roadmap" rel="noopener noreferrer"&gt;Complete Roadmap to QA Automation &amp;amp; Engineering&lt;/a&gt; organizes everything by series and learning order.&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next time you write an API test, I hope this guide saves you from the "it returned 200, ship it" trap. Test the structure, test the edge cases, and validate that schema.&lt;/p&gt;

</description>
      <category>qa</category>
      <category>testing</category>
      <category>restapi</category>
      <category>api</category>
    </item>
    <item>
      <title>I Started a YouTube Channel - Here's Why</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Wed, 08 Apr 2026 11:28:19 +0000</pubDate>
      <link>https://forem.com/idavidov13/i-started-a-youtube-channel-heres-why-5cjn</link>
      <guid>https://forem.com/idavidov13/i-started-a-youtube-channel-heres-why-5cjn</guid>
      <description>&lt;p&gt;After writing 30+ articles about Playwright and TypeScript, I always had the feeling that something was missing. The articles give you the patterns and the code - but the deeper explanations, the architectural reasoning, the "why behind the why" - that needed a different format.&lt;/p&gt;

&lt;p&gt;Written tutorials are great for reference. You bookmark them, copy the code blocks, and come back when you need a refresher. But some things just click faster when someone walks you through the reasoning - why this approach and not that one, what trade-offs were made, and what the architecture actually solves.&lt;/p&gt;

&lt;p&gt;That's why I created &lt;a href="https://www.youtube.com/@archqa" rel="noopener noreferrer"&gt;&lt;strong&gt;ArchQA&lt;/strong&gt;&lt;/a&gt; - a YouTube channel where I break down the fundamental architecture behind every decision, so you don't just copy the code - you understand why it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 Why Video?
&lt;/h2&gt;

&lt;p&gt;I've spent a lot of time trying to make my articles as practical as possible. Code blocks, correct vs. incorrect examples, real project structures. But there's a gap that text can't fully close.&lt;/p&gt;

&lt;p&gt;When you read about setting up a Page Object Model, you get the pattern. But when someone explains &lt;em&gt;why&lt;/em&gt; that pattern exists - what problem it solves, what falls apart without it, and how it connects to the bigger architectural picture - that's when it truly clicks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding the "why" is the difference between following instructions and making your own decisions.&lt;/strong&gt; That's the experience I wanted to create. Not surface-level tutorials - detailed explanations of the reasoning behind every architectural choice, so you can apply the same thinking to your own projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  📺 What's on the Channel
&lt;/h2&gt;

&lt;p&gt;The channel launches with three playlists, each covering topics my readers already care about - but going deeper into the "why" behind every decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript for Automation QA (Without the Fluff)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not just syntax. The &lt;a href="https://www.youtube.com/playlist?list=PLYxABk1YARBwGAnMRlwJCt-TZ8rjbjh48" rel="noopener noreferrer"&gt;video playlist&lt;/a&gt; digs into why TypeScript matters for QA, how the type system protects your tests, and what architectural patterns make your automation code maintainable long-term. You'll understand the fundamentals, not just follow along.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playwright Framework&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building a professional framework isn't about copying a folder structure. The &lt;a href="https://www.youtube.com/playlist?list=PLYxABk1YARBy-GoFGWdUCUPxV9ZYOIzLx" rel="noopener noreferrer"&gt;video series&lt;/a&gt; explains the architecture behind every layer - why Page Object Model is structured this way, why fixtures solve dependency injection, why certain patterns scale and others collapse. You'll walk away understanding the reasoning, so you can make your own informed decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playwright Tips &amp;amp; Tricks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;33 self-contained, practical techniques. Each &lt;a href="https://www.youtube.com/playlist?list=PLYxABk1YARBwgGO1DtOFTcPJ39aDUvvsL" rel="noopener noreferrer"&gt;video&lt;/a&gt; focuses on one tip - short, focused, and immediately applicable. API interception, schema validation with Zod, visual masking, time travel - the kind of things that take your framework from "works" to "works well".&lt;/p&gt;




&lt;h2&gt;
  
  
  🤝 Blog + Channel = The Full Picture
&lt;/h2&gt;

&lt;p&gt;The blog isn't going anywhere. If anything, the two formats make each other better.&lt;/p&gt;

&lt;p&gt;The articles give you the practical reference - code blocks, patterns, step-by-step guides. The videos go deeper into the architecture and the thinking behind those patterns. Together, you get both the "how" and the "why."&lt;/p&gt;

&lt;p&gt;If you want a structured path through all the written content, the &lt;a href="https://idavidov.eu/roadmap" rel="noopener noreferrer"&gt;Complete Roadmap to QA Automation &amp;amp; Engineering&lt;/a&gt; has everything organized by series and learning order.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Come Along for the Ride
&lt;/h2&gt;

&lt;p&gt;The channel is brand new. No backlog of hundreds of videos, no algorithm magic. Just me and topics I genuinely care about explaining well.&lt;/p&gt;

&lt;p&gt;If you've found value in the articles, I think you'll find even more in hearing the full reasoning behind the concepts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/@archqa" rel="noopener noreferrer"&gt;Subscribe to ArchQA on YouTube&lt;/a&gt;&lt;/strong&gt; - and if there's a topic you want to see covered as a video, let me know in the comments.&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt; This is just the beginning. More playlists, more deep dives, and more practical content are on the way. See you on the channel.&lt;/p&gt;

</description>
      <category>qa</category>
      <category>playwright</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From Prompt to Passing Test: A Complete Agentic QA Session</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Wed, 25 Mar 2026 05:48:57 +0000</pubDate>
      <link>https://forem.com/idavidov13/from-prompt-to-passing-test-a-complete-agentic-qa-session-4c46</link>
      <guid>https://forem.com/idavidov13/from-prompt-to-passing-test-a-complete-agentic-qa-session-4c46</guid>
      <description>&lt;p&gt;Sound familiar? In the &lt;a href="https://idavidov.eu/the-scaffold-playwright-ai" rel="noopener noreferrer"&gt;first article&lt;/a&gt;, we set up a project scaffold designed for AI. But a good structure only gets you so far if the AI is just a code suggester. Useful, but not transformative. You still have to know what to ask, verify what it wrote, adapt it to your project, and repeat for every file.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/what-is-agentic-qa" rel="noopener noreferrer"&gt;second article&lt;/a&gt;, we saw what makes an AI agent different from a chatbot. It reads your code, takes actions, and works inside your project. But here's the catch: an agent is only as good as the instructions it follows.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/claude-md-teaching-the-ai-your-rules" rel="noopener noreferrer"&gt;third article&lt;/a&gt;, we saw how &lt;code&gt;CLAUDE.md&lt;/code&gt; gives the agent its rules and workflow. But rules without depth only get you so far. "Use the Page Object Model" is a rule, but how exactly do you structure a page object? What's the difference between a locator getter and an action method? How do you compose components into page objects?&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/skills-domain-expertise-on-demand" rel="noopener noreferrer"&gt;fourth article&lt;/a&gt;, we gave the agent deep expertise through skill files. Now it knows &lt;em&gt;how&lt;/em&gt; to build page objects, selectors, and fixtures. But there's still a gap: the agent has never seen your application.&lt;/p&gt;

&lt;p&gt;Everything in this series has been building toward this moment. You have a scaffold with clear structure. An AI agent that reads your rules. Skills that give it deep expertise. And in the &lt;a href="https://idavidov.eu/explore-first-the-browser-use-workflow" rel="noopener noreferrer"&gt;fift article&lt;/a&gt;, a browser tool that lets it see your application.&lt;/p&gt;




&lt;p&gt;Now let's watch it work.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 The Scenario
&lt;/h2&gt;

&lt;p&gt;Your team has just shipped a user registration feature. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A page object for the registration form&lt;/li&gt;
&lt;li&gt;A Faker factory for user data&lt;/li&gt;
&lt;li&gt;A smoke test for the happy path&lt;/li&gt;
&lt;li&gt;A regression test for validation errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before agentic QA, this would be half a day of boilerplate. Inspecting the DOM, writing locators, setting up factories, wiring fixtures, writing tests, debugging failures.&lt;/p&gt;

&lt;p&gt;With an agent, your entire input is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The app has a new user registration page at &lt;code&gt;/register&lt;/code&gt;. Create a page object, a user factory, a smoke test, and regression tests for validation errors".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63u03nh3kqd13mqoh6l0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63u03nh3kqd13mqoh6l0.webp" alt="The Scenario"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 What the Agent Does: Step by Step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Read the rules&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent opens &lt;code&gt;CLAUDE.md&lt;/code&gt; first. It notes the mandatory rules, the workflow, and the skills it needs to load. For this task: &lt;code&gt;playwright-cli&lt;/code&gt;, &lt;code&gt;selectors&lt;/code&gt;, &lt;code&gt;page-objects&lt;/code&gt;, &lt;code&gt;test-standards&lt;/code&gt;, and &lt;code&gt;data-strategy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Explore the application&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent → playwright-cli:
"Navigate to /register. List all form fields (role, label, placeholder),
buttons (role, name), headings, and any validation messages visible."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discovery result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heading: "Create your account" (role: heading, level: 1)
Input: label "First name" - required
Input: label "Last name" - required
Input: label "Email address" - required, type: email
Input: label "Password" - required, type: password, placeholder: "At least 8 characters"
Input: label "Confirm password" - required, type: password
Button: "Create account" (role: button)
Link: "Already have an account? Sign in" (role: link)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Check existing patterns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent reads &lt;code&gt;pages/app/&lt;/code&gt; to see how existing page objects are structured. It reads &lt;code&gt;test-data/factories/app/&lt;/code&gt; to see how existing factories are formatted. It confirms the import paths and naming conventions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Generate the page object&lt;/strong&gt;&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="c1"&gt;// pages/app/registration.page.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationPage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;firstNameInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;lastNameInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Last name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;emailInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email address&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;passwordInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Password&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;confirmPasswordInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Confirm password&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;createAccountButton&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create account&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;firstNameError&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;First name is required&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;async&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RegistrationData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstNameInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastNameInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passwordInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmPasswordInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createAccountButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/register&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;&lt;strong&gt;Step 5: Register the page object&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent adds &lt;code&gt;registrationPage&lt;/code&gt; to &lt;code&gt;fixtures/pom/page-object-fixture.ts&lt;/code&gt; so tests can access it via dependency injection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Generate the factory&lt;/strong&gt;&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="c1"&gt;// test-data/factories/app/user.factory.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@faker-js/faker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstName&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;lastName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;faker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;password&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;memorable&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="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;&lt;strong&gt;Step 7: Generate the smoke test&lt;/strong&gt;&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="c1"&gt;// tests/app/functional/registration.spec.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixtures/pom/test-options&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-data/factories/app/user.factory&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new user can register successfully&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="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@smoke&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="nx"&gt;registrationPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dashboardPage&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUser&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;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Given the user is on the registration page&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registrationPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;When they fill in valid details and submit&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registrationPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&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="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Then they are redirected to the dashboard&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dashboardPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;welcomeHeading&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 8: Generate the regression tests&lt;/strong&gt;&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;import&lt;/span&gt; &lt;span class="nx"&gt;validationCases&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-data/static/app/registrationErrors.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;validationCases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`shows error when &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is invalid`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@regression&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="nx"&gt;registrationPage&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;registrationPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;registrationPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitWithInvalid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&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;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registrationPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errorFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 9: Run the tests&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test &lt;/span&gt;tests/app/functional/registration.spec.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Running 1 test using 1 worker
  ✓ new user can register successfully (2.3s)
  ✓ shows error when email is invalid (1.1s)
  ✓ shows error when password is too short (1.0s)
  ✓ shows error when first name is empty (0.9s)

  4 passed (6.2s)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reports back: all tests pass. Task complete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftj72k2ivvakvgy7bzas8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftj72k2ivvakvgy7bzas8.webp" alt="What the Agent Does"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  👀 What You Review
&lt;/h2&gt;

&lt;p&gt;You didn't write any of that. But your job isn't done. You review it.&lt;/p&gt;

&lt;p&gt;What you're looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do the locators match what you see in the app?&lt;/strong&gt; The agent used real discovery, but you know your application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the test actually testing the right thing?&lt;/strong&gt; The happy path asserts the dashboard heading. Is that the right success indicator?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are the edge cases covered?&lt;/strong&gt; The regression tests came from a static JSON file. Did the agent create sensible validation cases?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it fit the codebase style?&lt;/strong&gt; Compare against existing tests. Does this look like it belongs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This review takes 5-10 minutes. Writing everything from scratch would have taken half a day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3vjg6jbssk05al4rj5n.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3vjg6jbssk05al4rj5n.webp" alt="What You Review"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🌱 Growing the Framework With AI
&lt;/h2&gt;

&lt;p&gt;This workflow doesn't just apply to new features. The same pattern works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring.&lt;/strong&gt; "The navigation component was moved to a sidebar. Update the relevant page objects".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New API endpoints.&lt;/strong&gt; "The &lt;code&gt;/users&lt;/code&gt; endpoint now returns a &lt;code&gt;role&lt;/code&gt; field. Update the schema and any affected tests".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup.&lt;/strong&gt; "There are three page objects with duplicate navigation methods. Extract them into a shared component".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent reads the current state of the codebase, makes targeted changes, runs the affected tests, and confirms nothing broke. You review the diff.&lt;/p&gt;

&lt;p&gt;Over time, your role becomes less about writing tests and more about defining what &lt;em&gt;should&lt;/em&gt; be tested. The thinking part of QA, not the typing part.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;The scaffold, CLAUDE.md, the skills, the explore-first workflow are not a sorcery. They're just a well-designed system that makes it easy for an agent to do the right thing.&lt;/p&gt;

&lt;p&gt;The insight at the heart of agentic QA is simple: &lt;strong&gt;AI is most useful when it has clear constraints&lt;/strong&gt;. A blank slate produces inconsistent results. A scaffold with rules, skills, and a workflow produces output you can trust.&lt;/p&gt;

&lt;p&gt;You're not replacing the QA engineer. You're giving the QA engineer a tireless, fast, rule-following colleague who never complains about writing boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjsg97vmlymu73mtx7jf.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhjsg97vmlymu73mtx7jf.webp" alt="The Bigger Picture"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Get Started
&lt;/h2&gt;

&lt;p&gt;You have complete instructions to get started with the AI-assisted development.&lt;/p&gt;

&lt;p&gt;You can find the Public README.md file for the scaffold on GitHub: &lt;a href="https://github.com/idavidov13/Playwright-Scaffold-AI-Assisted-Development-Public" rel="noopener noreferrer"&gt;Playwright Scaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get access to the private GitHub repository here: &lt;a href="https://buymeacoffee.com/idavidov/e/513835" rel="noopener noreferrer"&gt;Get Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>qa</category>
      <category>testing</category>
      <category>agentskills</category>
      <category>ai</category>
    </item>
    <item>
      <title>Explore First: Why the Agent Looks Before It Writes</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Fri, 20 Mar 2026 08:11:28 +0000</pubDate>
      <link>https://forem.com/idavidov13/explore-first-why-the-agent-looks-before-it-writes-4lmb</link>
      <guid>https://forem.com/idavidov13/explore-first-why-the-agent-looks-before-it-writes-4lmb</guid>
      <description>&lt;p&gt;Sound familiar? In the &lt;a href="https://idavidov.eu/the-scaffold-playwright-ai" rel="noopener noreferrer"&gt;first article&lt;/a&gt;, we set up a project scaffold designed for AI. But a good structure only gets you so far if the AI is just a code suggester. Useful, but not transformative. You still have to know what to ask, verify what it wrote, adapt it to your project, and repeat for every file.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/what-is-agentic-qa" rel="noopener noreferrer"&gt;second article&lt;/a&gt;, we saw what makes an AI agent different from a chatbot. It reads your code, takes actions, and works inside your project. But here's the catch: an agent is only as good as the instructions it follows.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/claude-md-teaching-the-ai-your-rules" rel="noopener noreferrer"&gt;third article&lt;/a&gt;, we saw how &lt;code&gt;CLAUDE.md&lt;/code&gt; gives the agent its rules and workflow. But rules without depth only get you so far. "Use the Page Object Model" is a rule, but how exactly do you structure a page object? What's the difference between a locator getter and an action method? How do you compose components into page objects?&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/skills-domain-expertise-on-demand" rel="noopener noreferrer"&gt;fourth article&lt;/a&gt;, we gave the agent deep expertise through skill files. Now it knows &lt;em&gt;how&lt;/em&gt; to build page objects, selectors, and fixtures. But there's still a gap: the agent has never seen your application.&lt;/p&gt;




&lt;p&gt;Here's a scenario every automation engineer knows. You ask an AI to generate a page object for your login page. It confidently produces:&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;get&lt;/span&gt; &lt;span class="nf"&gt;loginButton&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login-btn&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;You run the test. It fails. The button doesn't have that test ID. It never did. The AI made it up because it had no way to know the real structure of your page.&lt;/p&gt;

&lt;p&gt;This is the core problem with AI code generation for UI testing: &lt;strong&gt;the AI is writing about a UI it has never seen&lt;/strong&gt;. The result is locators that look generally correct but don't work.&lt;/p&gt;

&lt;p&gt;The scaffold's answer to this is a principle called &lt;strong&gt;explore first&lt;/strong&gt;, and a tool called &lt;code&gt;playwright-cli&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 What Is playwright-cli?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;playwright-cli&lt;/code&gt; is a browser automation CLI that lets the AI agent control a real browser. Navigate to URLs, read the page's DOM, discover element roles and labels, take screenshots, and extract structured information.&lt;/p&gt;

&lt;p&gt;When an agent has &lt;code&gt;playwright-cli&lt;/code&gt;, it doesn't have to guess what's on your login page. It can go look.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The agent runs something like this before generating code&lt;/span&gt;
playwright-cli &lt;span class="s2"&gt;"Navigate to https://myapp.com/login and list all interactive
elements: their role, accessible name, and any associated label text"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What comes back is a real inventory of the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;heading&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in"&lt;/span&gt;
  &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;textbox&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Email&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;address"&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;textbox&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password"&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;button&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sign&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;link&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forgot&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;your&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;link&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Create&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;account"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the agent writes a page object, it uses real information:&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;get&lt;/span&gt; &lt;span class="nf"&gt;emailInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email address&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;passwordInput&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Password&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="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;signInButton&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sign in&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;These locators work on the first run. No guessing, no iteration, no debugging brittle selectors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4v3o390dr0xspcky0uxz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4v3o390dr0xspcky0uxz.webp" alt="Exploration" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ The Explore-First Workflow
&lt;/h2&gt;

&lt;p&gt;The scaffold's &lt;code&gt;CLAUDE.md&lt;/code&gt; makes exploration a required step before any code generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For UI pages:
1. Use playwright-cli to navigate to the target URL
2. Discover: element roles, accessible names, label text, form structure
3. Note any dynamic content or state-dependent elements
4. Only then: generate the page object

For API endpoints:
1. Make a real request to the endpoint
2. Capture: field names, data types, optional vs required fields
3. Note the exact error response structure
4. Only then: generate the Zod schema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Skip exploration only&lt;/strong&gt; when the user has already provided the exact structure. Everything else, the agent goes to look first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vhbq7ertftnc2oesbtj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vhbq7ertftnc2oesbtj.webp" alt="Explore first workflow" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 What the Agent Discovers Beyond Selectors
&lt;/h2&gt;

&lt;p&gt;A browser exploration session doesn't just find locators. A thorough agent also discovers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Navigation flows.&lt;/strong&gt; What happens after you click "Sign in"? Where does the page go? What element should the test assert against to confirm success?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Form validation.&lt;/strong&gt; Does the form validate on blur or on submit? What do the error messages actually say? ("Email is required" or "Please enter your email address"?)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic content.&lt;/strong&gt; Is there a loading spinner? A toast notification? An element that only appears after an API call? These affect how the test should wait for state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Page structure.&lt;/strong&gt; Is the "Settings" link in a sidebar, a dropdown, or a navigation bar? This determines whether it belongs in the page object or a shared component.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this context shapes better tests, tests that reflect how the application actually works, not how the agent imagined it might work.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔌 Exploration for APIs
&lt;/h2&gt;

&lt;p&gt;The same principle applies to API testing. Before the agent writes a Zod schema (if the API documentation is not available), it makes a real request to the endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;# The agent calls the actual API
POST /auth/login
{ "email": "test@example.com", "password": "secret" }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-01T00:00:00.000Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the schema is built from reality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LoginResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strictObject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every field name is correct. Every type is verified. &lt;code&gt;z.strictObject()&lt;/code&gt; means if the API later adds an unexpected field, the test flags it immediately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki547j77y799okdrfpe8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fki547j77y799okdrfpe8.webp" alt="Exploration for APIs" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚠️ When Exploration Reveals Surprises
&lt;/h2&gt;

&lt;p&gt;Sometimes what the agent finds is not what you expected, and that's valuable information.&lt;/p&gt;

&lt;p&gt;The label on the form says "E-mail" (with a hyphen), not "Email". The button says "Log in", not "Login". The error message says "Your password is incorrect." with a trailing period. These small differences matter for locators and assertions.&lt;/p&gt;

&lt;p&gt;Without exploration, the agent would have guessed and been wrong. With exploration, it finds the truth, and so do you.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧑‍💻 Your Role in the Explore-First Loop
&lt;/h2&gt;

&lt;p&gt;With this workflow, your job shifts. You're not writing locators. You're directing exploration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; You open DevTools, inspect the DOM, copy selectors, paste them, test them, adjust them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; You tell the agent what page to explore and what to generate. You review the output, confirm the locators look right, run the tests.&lt;/p&gt;

&lt;p&gt;The agent does the tedious part. You do the thinking part.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmv8ukfxytlsfh4xvz1sp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmv8ukfxytlsfh4xvz1sp.webp" alt="Your Role in the Explore-First Loop" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt; The explore-first principle is what makes AI-generated tests reliable instead of plausible. In the final article, we bring everything together: a complete end-to-end example of an agentic QA session from prompt to passing test.&lt;/p&gt;

&lt;p&gt;You can find the Public README.md file for the scaffold on GitHub: &lt;a href="https://github.com/idavidov13/Playwright-Scaffold-AI-Assisted-Development-Public" rel="noopener noreferrer"&gt;Playwright Scaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get access to the private GitHub repository here: &lt;a href="https://buymeacoffee.com/idavidov/e/513835" rel="noopener noreferrer"&gt;Get Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>qa</category>
      <category>testing</category>
      <category>agentskills</category>
      <category>ai</category>
    </item>
    <item>
      <title>Skills: Domain Expertise on Demand</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Fri, 13 Mar 2026 06:11:57 +0000</pubDate>
      <link>https://forem.com/idavidov13/skills-domain-expertise-on-demand-4e52</link>
      <guid>https://forem.com/idavidov13/skills-domain-expertise-on-demand-4e52</guid>
      <description>&lt;p&gt;Sound familiar? In the &lt;a href="https://idavidov.eu/the-scaffold-playwright-ai" rel="noopener noreferrer"&gt;first article&lt;/a&gt;, we set up a project scaffold designed for AI. But a good structure only gets you so far if the AI is just a code suggester. Useful, but not transformative. You still have to know what to ask, verify what it wrote, adapt it to your project, and repeat for every file.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/what-is-agentic-qa" rel="noopener noreferrer"&gt;second article&lt;/a&gt;, we saw what makes an AI agent different from a chatbot. It reads your code, takes actions, and works inside your project. But here's the catch: an agent is only as good as the instructions it follows.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/claude-md-teaching-the-ai-your-rules" rel="noopener noreferrer"&gt;third article&lt;/a&gt;, we saw how &lt;code&gt;CLAUDE.md&lt;/code&gt; gives the agent its rules and workflow. But rules without depth only get you so far. "Use the Page Object Model" is a rule, but how exactly do you structure a page object? What's the difference between a locator getter and an action method? How do you compose components into page objects?&lt;/p&gt;




&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; gives the agent its rules and workflow. But rules without depth only get you so far. "Use the Page Object Model" is a rule, but how exactly do you structure a page object? What's the difference between a locator getter and an action method? How do you compose components into page objects?&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;skill files&lt;/strong&gt; come in.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 What Is a Skill File?
&lt;/h2&gt;

&lt;p&gt;A skill file is a detailed, focused markdown document that covers one area of your testing framework in depth. Where &lt;code&gt;CLAUDE.md&lt;/code&gt; tells the agent &lt;em&gt;what&lt;/em&gt; to do, skills tell the agent &lt;em&gt;how&lt;/em&gt; to do it.&lt;/p&gt;

&lt;p&gt;The scaffold comes with 13 skills, all living in &lt;code&gt;.claude/skills/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/skills/
├── selectors/SKILL.md        → Locator priority, forbidden patterns, examples
├── page-objects/SKILL.md     → POM structure, getters vs actions, registration
├── fixtures/SKILL.md         → DI pattern, fixture creation, merging
├── test-standards/SKILL.md   → Test structure, imports, tagging, steps
├── api-testing/SKILL.md      → apiRequest fixture, schema validation
├── data-strategy/SKILL.md    → Factories vs static JSON, Faker usage
├── type-safety/SKILL.md      → Zod schemas, TypeScript strict mode
├── enums/SKILL.md            → Enum conventions and naming
├── config/SKILL.md           → Config patterns, environment variables
├── helpers/SKILL.md          → Helper functions, auth helpers
├── browser-use/SKILL.md      → Live app exploration with browser automation
├── common-tasks/SKILL.md     → Prompt templates, verification checklist
└── refactor-values/SKILL.md  → Safe refactoring of enums and static data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one is loaded on demand, not all at once. The agent reads only the skill that's relevant to what it's currently doing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcswcd8xp97oxvrnwyd29.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcswcd8xp97oxvrnwyd29.webp" alt="Concept of skill file" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Why Not Put Everything in CLAUDE.md?
&lt;/h2&gt;

&lt;p&gt;A single giant instruction file has a real problem. The further down the page a rule lives, the less likely the AI is to apply it consistently. Context windows have limits, and even when they don't, a 5,000 word instruction file is harder to reason about than a 300 word one.&lt;/p&gt;

&lt;p&gt;Skills solve this with &lt;strong&gt;lazy loading&lt;/strong&gt;. The agent reads the orchestrator (&lt;code&gt;CLAUDE.md&lt;/code&gt;) first. Short, high-level, fast. When it needs to work on a specific area, it reads the relevant skill. Deep expertise, loaded exactly when needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent working on a page object:
  → CLAUDE.md: "Read selectors and page-objects skills for pages/**"
  → Loads selectors/SKILL.md
  → Loads page-objects/SKILL.md
  → Now has full expertise for this specific task
  → Generates code confidently and correctly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7mpp6k25inkmnaysx9p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7mpp6k25inkmnaysx9p.webp" alt="Concept of separate files" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📖 What's Inside a Skill File?
&lt;/h2&gt;

&lt;p&gt;Let's look at what the &lt;code&gt;selectors&lt;/code&gt; skill actually contains:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The priority rule with reasoning:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Priority order:
&lt;span class="p"&gt;
1.&lt;/span&gt; getByRole() - Tests behaviour, survives CSS/ID changes
&lt;span class="p"&gt;2.&lt;/span&gt; getByLabel() - Tied to accessible form labels
&lt;span class="p"&gt;3.&lt;/span&gt; getByPlaceholder() - Tied to input hint text
&lt;span class="p"&gt;4.&lt;/span&gt; getByText() - Matches visible text content
&lt;span class="p"&gt;5.&lt;/span&gt; getByTestId() - Last resort, when no semantic option exists

Forbidden: XPath, CSS class selectors (.btn-primary)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Examples of correct vs incorrect usage:&lt;/strong&gt;&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="c1"&gt;// ✅ Correct&lt;/span&gt;
&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Submit Order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email address&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Wrong: fragile, will break on CSS changes&lt;/span&gt;
&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.submit-btn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#email-input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xpath=//button[@class="btn"]&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;&lt;strong&gt;Edge cases:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Buttons with icons only: use getByRole('button', { name: /icon label/i })
&lt;span class="p"&gt;-&lt;/span&gt; Dynamic text: use getByRole with partial name match
&lt;span class="p"&gt;-&lt;/span&gt; Inside a shadow DOM: note the limitation, escalate to human review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A skill file is essentially what you'd tell a new team member on their first code review. "We do it this way, here's why, here are the gotchas".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy69pg4m0734br308gslp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy69pg4m0734br308gslp.webp" alt="Concept of single file" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 The &lt;code&gt;common-tasks&lt;/code&gt; Skill
&lt;/h2&gt;

&lt;p&gt;One skill deserves special attention: &lt;code&gt;common-tasks&lt;/code&gt;. It contains &lt;strong&gt;prompt templates&lt;/strong&gt;, pre-written instructions for the most frequent tasks an agent performs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Create a Page Object&lt;/span&gt;

Before starting:
&lt;span class="p"&gt;
1.&lt;/span&gt; Read selectors/SKILL.md and page-objects/SKILL.md
&lt;span class="p"&gt;2.&lt;/span&gt; Navigate to the page using browser-use
&lt;span class="p"&gt;3.&lt;/span&gt; Discover: button names, label text, heading roles

Generate:
&lt;span class="p"&gt;
-&lt;/span&gt; pages/{area}/{name}.page.ts
&lt;span class="p"&gt;-&lt;/span&gt; Register in fixtures/pom/page-object-fixture.ts

Verification checklist:
&lt;span class="p"&gt;
-&lt;/span&gt; [ ] No XPath
&lt;span class="p"&gt;-&lt;/span&gt; [ ] No hardcoded strings in test files
&lt;span class="p"&gt;-&lt;/span&gt; [ ] No &lt;span class="sb"&gt;`any`&lt;/span&gt; type
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Linting passes
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Tests run and pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the agent's playbook. Instead of reasoning from scratch about how to create a page object, it has a structured checklist to follow. The output is consistent because the process is consistent.&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Writing Your Own Skill Files
&lt;/h2&gt;

&lt;p&gt;The skills that ship with the scaffold are a starting point. As your project grows, you'll discover new conventions that need documenting, and new anti-patterns that need blocking.&lt;/p&gt;

&lt;p&gt;A good skill file has four parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The rule:&lt;/strong&gt; What the agent should do (or not do) in this area&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The reasoning:&lt;/strong&gt; Why the rule exists (helps the agent generalize)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code examples:&lt;/strong&gt; Correct and incorrect, clearly labeled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases:&lt;/strong&gt; The situations where the rule gets tricky&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep them short. A skill file that is short and concise will get used. One that is long and verbose won't.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 Skills as a Team Knowledge Base
&lt;/h2&gt;

&lt;p&gt;Here's something that's easy to miss. Skill files aren't just for AI. They're documentation of your team's decisions.&lt;/p&gt;

&lt;p&gt;When a new colleague joins your team, they can read the skill files to understand why things are done the way they are. When you make a decision in a code review ("we always use &lt;code&gt;z.strictObject()&lt;/code&gt; because here's what happened that one time"), you add it to the relevant skill file. The AI and the humans stay aligned.&lt;/p&gt;

&lt;p&gt;Over time, the &lt;code&gt;.claude/skills/&lt;/code&gt; folder becomes a living record of your team's accumulated QA knowledge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9csc28w6a6m9uktlknb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff9csc28w6a6m9uktlknb.webp" alt="Concept of team knowledge base" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt; Skills are what separate a consistent AI-generated codebase from a chaotic one. But rules and skills only get the agent so far. It also needs to see the actual application before writing code for it. That's the subject of the next article.&lt;/p&gt;

&lt;p&gt;You can find the Public README.md file for the scaffold on GitHub: &lt;a href="https://github.com/idavidov13/Playwright-Scaffold-AI-Assisted-Development-Public" rel="noopener noreferrer"&gt;Playwright Scaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get access to the private GitHub repository here: &lt;a href="https://buymeacoffee.com/idavidov/e/513835" rel="noopener noreferrer"&gt;Get Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>qa</category>
      <category>testing</category>
      <category>agents</category>
    </item>
    <item>
      <title>CLAUDE.md: Teaching the AI Your Rules</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Fri, 06 Mar 2026 06:03:57 +0000</pubDate>
      <link>https://forem.com/idavidov13/claudemd-teaching-the-ai-your-rules-2jcd</link>
      <guid>https://forem.com/idavidov13/claudemd-teaching-the-ai-your-rules-2jcd</guid>
      <description>&lt;p&gt;Sound familiar? In the &lt;a href="https://idavidov.eu/the-scaffold-playwright-ai" rel="noopener noreferrer"&gt;first article&lt;/a&gt;, we set up a project scaffold designed for AI. But a good structure only gets you so far if the AI is just a code suggester. Useful, but not transformative. You still have to know what to ask, verify what it wrote, adapt it to your project, and repeat for every file.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://idavidov.eu/what-is-agentic-qa" rel="noopener noreferrer"&gt;second article&lt;/a&gt;, we saw what makes an AI agent different from a chatbot. It reads your code, takes actions, and works inside your project. But here's the catch: an agent is only as good as the instructions it follows.&lt;/p&gt;




&lt;p&gt;Every team has conventions. Use this selector pattern. Put files here. Never do that. Import from this file, not that one.&lt;/p&gt;

&lt;p&gt;The problem is that conventions live in people's heads. A new team member, human or AI, doesn't know them until they make a mistake. With humans, you do a code review and explain. With AI, you get the same mistake every single time, because it resets to zero with every new conversation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; solves this. It's a file that the AI reads at the start of every session, every time, before generating a single line of code.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 What Is CLAUDE.md?
&lt;/h2&gt;

&lt;p&gt;It's a markdown file at the root of the project, but it's not documentation for humans. It's an &lt;strong&gt;instruction set for an AI agent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When Claude Code (or any Claude-based agent) opens your project, it reads &lt;code&gt;CLAUDE.md&lt;/code&gt; first. Everything in that file shapes how the agent behaves throughout the session. What patterns it follows, what it checks before writing, what it refuses to do.&lt;/p&gt;

&lt;p&gt;Think of it as onboarding documentation that actually gets read.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not just for Claude.&lt;/strong&gt; The scaffold provides the same rules in three formats: &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code, &lt;code&gt;.cursor/rules/&lt;/code&gt; for Cursor, and &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; for GitHub Copilot. Same architecture, different file paths. This article uses &lt;code&gt;CLAUDE.md&lt;/code&gt; as the example, but the pattern applies regardless of which AI tool you use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcxwxgleryxl61n2yjp3q.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcxwxgleryxl61n2yjp3q.webp" alt="No Rules vs Claude.md"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏛️ The Constitution Pattern
&lt;/h2&gt;

&lt;p&gt;The scaffold organizes &lt;code&gt;CLAUDE.md&lt;/code&gt; around three tiers of rules, a structure borrowed from how legal systems separate mandatory law from guidance to prohibition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MUST    → Mandatory. No exceptions, no debate.
SHOULD  → Recommended. The agent prefers this unless there's a reason not to.
WON'T   → Forbidden. The agent refuses to do this regardless of what's asked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This three-tier structure is important. If everything is mandatory, the agent has no room to exercise judgment. If everything is flexible, the agent defaults to its own habits, which may not match yours. The tiers give the agent a clear mental model of what's negotiable and what isn't.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0nb3mjjedemjmict8bb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0nb3mjjedemjmict8bb.webp" alt="Constitution Pattern"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ The MUST Rules
&lt;/h2&gt;

&lt;p&gt;These are non-negotiable. The scaffold's mandatory rules cover the things that, if violated, break the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Rule                 | Requirement                                                   |
| -------------------- | ------------------------------------------------------------- |
| Dependency Injection | Never use &lt;span class="sb"&gt;`new PageObject(page)`&lt;/span&gt; in tests                     |
| Imports              | Import &lt;span class="sb"&gt;`test`&lt;/span&gt; and &lt;span class="sb"&gt;`expect`&lt;/span&gt; from fixtures/pom/test-options.ts  |
| Selectors            | getByRole() &amp;gt; getByLabel() &amp;gt; getByPlaceholder() &amp;gt; getByText() |
| Type Safety          | Use Zod schemas in fixtures/api/schemas/, no &lt;span class="sb"&gt;`any`&lt;/span&gt; type       |
| Strict Schemas       | Always use z.strictObject(), never z.object()                 |
| Assertions           | Web-first assertions only, never waitForTimeout()             |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how specific these are. "Use semantic selectors" is too vague. The agent has to interpret what that means. "Use &lt;code&gt;getByRole()&lt;/code&gt; before &lt;code&gt;getByLabel()&lt;/code&gt; before &lt;code&gt;getByPlaceholder()&lt;/code&gt;" is unambiguous. The agent follows a decision tree.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 The SHOULD Rules
&lt;/h2&gt;

&lt;p&gt;These are recommendations for when the agent has a choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Rule            | Recommendation                                                |
| --------------- | ------------------------------------------------------------- |
| Explore First   | Navigate to the page or endpoint before generating code       |
| Data Generation | Use Faker via factories for all happy-path test data          |
| Test Isolation  | Tests should be independent. Use beforeEach, not shared state |
| Test Steps      | Use Given/When/Then structure with test.step()                |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Explore First" is here rather than in MUST because there are legitimate cases where you've already provided the element structure. The agent uses its judgment.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚫 The WON'T Rules
&lt;/h2&gt;

&lt;p&gt;These are hard blocks, things the agent should refuse to do even if you ask nicely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Rule                 | Violation                                      |
| -------------------- | ---------------------------------------------- |
| No XPath             | Never use XPath selectors                      |
| No Hard Waits        | Never use page.waitForTimeout()                |
| No &lt;span class="sb"&gt;`any`&lt;/span&gt;             | Never use TypeScript's any type                |
| No Multiple Tags     | Each test has exactly ONE tag                  |
| No Hardcoded Content | Never hardcode test strings. Use Faker instead |
| No Loose Schemas     | Never use z.object(), always z.strictObject()  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WON'T list is where institutional knowledge lives. These are the mistakes your team has made (or seen others make) and decided to never repeat. Writing them down here means the AI inherits that hard-won experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ The Workflow Section
&lt;/h2&gt;

&lt;p&gt;Beyond rules, &lt;code&gt;CLAUDE.md&lt;/code&gt; also defines a &lt;strong&gt;step-by-step workflow&lt;/strong&gt; the agent follows for every code generation task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Read this file (always loaded)
2. Explore the application (navigate or make API requests)
3. Find existing code to follow as a pattern
4. Use fixtures, never manual instantiation
5. Generate data with factories
6. Check against the WON'T rules
7. Run the tests. Don't report done until they pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 7 is particularly important. Without it, the agent might generate code that looks right but fails at runtime. With it, the agent runs the tests and confirms they pass before telling you the task is done.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb8kr8niaaefv30od8414.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb8kr8niaaefv30od8414.webp" alt="The Workflow Section"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📁 The Skills Index
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; also acts as a map to the skills files. Rather than cramming every detail into one file (which would be overwhelming), it tells the agent: &lt;em&gt;"When you're working on page objects, read the page-objects skill. When you're working on API schemas, read the api-testing skill."&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Skill          | Read When Working On               |
| -------------- | ---------------------------------- |
| selectors      | pages/&lt;span class="se"&gt;\*\*&lt;/span&gt;                         |
| page-objects   | pages/&lt;span class="se"&gt;\*\*&lt;/span&gt;                         |
| fixtures       | fixtures/&lt;span class="se"&gt;\*\*&lt;/span&gt;                      |
| test-standards | tests/&lt;span class="se"&gt;\*\*&lt;/span&gt;                         |
| api-testing    | fixtures/api/&lt;span class="gs"&gt;**, tests/**&lt;/span&gt;/api/&lt;span class="se"&gt;\*\*&lt;/span&gt; |
| data-strategy  | test-data/&lt;span class="se"&gt;\*\*&lt;/span&gt;                     |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps &lt;code&gt;CLAUDE.md&lt;/code&gt; focused and fast to parse, while the detailed expertise lives in dedicated skill files. That brings us to the next article.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdm4h5g90lvkgcy3li2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbdm4h5g90lvkgcy3li2.webp" alt="The Skills Index"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✍️ Writing Your Own CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;You don't have to start from scratch. The scaffold's &lt;code&gt;CLAUDE.md&lt;/code&gt; is a template. To adapt it to your project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Change the file paths&lt;/strong&gt; to match your actual folder names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add your team's specific WON'Ts&lt;/strong&gt;: the anti-patterns you've personally seen cause problems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the workflow&lt;/strong&gt; if your project has a setup step or a specific CI check to run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it honest&lt;/strong&gt;: don't add rules you don't actually enforce, or the agent will contradict your existing codebase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal isn't a perfect document on day one. It's a living file that gets better every time you catch the agent doing something you don't want.&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt; is the file that turns a general AI into a specialized team member. But rules alone aren't enough. The agent also needs deep expertise for specific tasks. That's what skill files provide. See you in the next one.&lt;/p&gt;

&lt;p&gt;You can find the Public README.md file for the scaffold on GitHub: &lt;a href="https://github.com/idavidov13/Playwright-Scaffold-AI-Assisted-Development-Public" rel="noopener noreferrer"&gt;Playwright Scaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get access to the private GitHub repository here: &lt;a href="https://buymeacoffee.com/idavidov/e/513835" rel="noopener noreferrer"&gt;Get Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>agents</category>
      <category>qa</category>
    </item>
    <item>
      <title>What Is Agentic QA and Why It Changes Everything</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Wed, 04 Mar 2026 06:17:27 +0000</pubDate>
      <link>https://forem.com/idavidov13/what-is-agentic-qa-and-why-it-changes-everything-pbl</link>
      <guid>https://forem.com/idavidov13/what-is-agentic-qa-and-why-it-changes-everything-pbl</guid>
      <description>&lt;p&gt;Sound familiar? In the &lt;a href="https://idavidov.eu/the-scaffold-playwright-ai" rel="noopener noreferrer"&gt;first article&lt;/a&gt;, we set up a project scaffold designed for AI. But a good structure only gets you so far if the AI is just a code suggester. Useful, but not transformative. You still have to know what to ask, verify what it wrote, adapt it to your project, and repeat for every file.&lt;/p&gt;




&lt;p&gt;"I asked Claude Code to write a test and it gave me something that kind of works"&lt;/p&gt;

&lt;p&gt;Sound familiar? That's AI as a &lt;strong&gt;code suggester&lt;/strong&gt;. Useful, but not transformative. You still have to know what to ask, verify what it wrote, adapt it to your project, and repeat for every file.&lt;/p&gt;

&lt;p&gt;Agentic QA is different. Instead of an AI that answers questions, you have an AI that &lt;strong&gt;takes actions&lt;/strong&gt;. It reads your codebase, opens a browser, navigates your application, discovers elements, and generates code that fits directly into your existing structure.&lt;/p&gt;

&lt;p&gt;The difference is the difference between a search engine and an employee.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Agent vs Chatbot: What's the Actual Difference?
&lt;/h2&gt;

&lt;p&gt;A chatbot responds to a single message. It has no memory of your project, no access to your files, and no ability to take actions beyond generating text.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;agent&lt;/strong&gt; operates in a loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It receives a goal (create a test suite for the registration page)&lt;/li&gt;
&lt;li&gt;It takes actions to gather information (reads files, navigates the browser)&lt;/li&gt;
&lt;li&gt;It uses what it found to produce an output (generates the page object, Faker factory, test suite, etc.)&lt;/li&gt;
&lt;li&gt;It verifies the output (runs the tests, checks for linting errors)&lt;/li&gt;
&lt;li&gt;It iterates if something is wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiilsfhan8q7q4l6tsx0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiilsfhan8q7q4l6tsx0w.png" alt="ChatBot vs Agent"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The agent isn't just writing code. It's operating inside your development environment, making decisions based on real information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You: Add a e2e test for the checkout flow

Agent:
  → Opens browser, navigates to the product page
  → Discovers: all the necessary actions to complete the checkout flow
  → Reads pages/ to understand existing page object conventions
  → Reads tests/ to understand the test structure and imports
  → Reads .claude/skills/test-standards to check test standards
  → Generates checkout.page.ts, checkout.spec.ts, factory functions, Zod schemas, etc.
  → Runs the test: npx playwright test checkout.spec.ts
  → Confirms: 1 passed, 0 failed
  → If something is wrong, it reads the error and iterates again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That entire loop happens without you writing a line. The agent is doing all the work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wuq1195ckwjmpoiqxx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wuq1195ckwjmpoiqxx7.png" alt="Agentic Loop"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Why Test Automation Is a Perfect Fit
&lt;/h2&gt;

&lt;p&gt;Not every software task is well-suited for agents. But test automation has properties that make it almost ideal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's highly structured.&lt;/strong&gt; Tests follow patterns. Page objects follow patterns. Data factories follow patterns. Patterns are exactly what AI agents are good at recognizing and reproducing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's verifiable.&lt;/strong&gt; After generating a test, the agent can run it. A passing test is objective confirmation that the output is correct. The agent doesn't have to guess. It can check.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's repetitive.&lt;/strong&gt; Writing a page object for the tenth page in your app involves the same thinking as the first. That repetition is tedious for humans and trivial for agents.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It requires codebase context.&lt;/strong&gt; A good test has to fit the existing project. An agent that can read your files produces output that integrates cleanly. A chatbot produces output you have to adapt manually.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Important to note that "Human in the loop" is still mandatory. The agent is not a replacement for a human. It is a tool to help you do your job faster and better. You still need to review the output and decide what to do next.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xwi83xsa6ili06zyjcg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xwi83xsa6ili06zyjcg.png" alt="Why Agents are perfect fit for Automation"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 The Mental Shift
&lt;/h2&gt;

&lt;p&gt;The biggest change is not technical. It is how you think about your role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before agentic QA:&lt;/strong&gt; You are the writer. You design the test, write the page object, wire up the fixture, create the factory, run the tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With agentic QA:&lt;/strong&gt; You are the reviewer and director. You built the architecture. You define the goal, review the output, catch anything the agent missed, and decide what to test next.&lt;/p&gt;

&lt;p&gt;This doesn't mean less work. It means different work, higher-level work. You spend more time thinking about &lt;em&gt;what&lt;/em&gt; to test and less time typing boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb911nqsgolikwmr4uv4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb911nqsgolikwmr4uv4.png" alt="The Mental Shift"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ What Makes an Agent Reliable?
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you. A raw AI agent, pointed at your codebase with no guidance, will produce inconsistent results. Sometimes good, sometimes generic, sometimes wrong in subtle ways.&lt;/p&gt;

&lt;p&gt;What makes an agent reliable is &lt;strong&gt;context and constraints&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rules about what it must always do&lt;/li&gt;
&lt;li&gt;Rules about what it must never do&lt;/li&gt;
&lt;li&gt;Detailed expertise for specific tasks&lt;/li&gt;
&lt;li&gt;A workflow it follows before generating code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this scaffold, all of that lives in two places: &lt;code&gt;CLAUDE.md&lt;/code&gt; (the orchestrator) and &lt;code&gt;.claude/skills/&lt;/code&gt; (the expertise files). Together they turn a general-purpose AI into something that behaves like a senior QA engineer who has been on your project for months.&lt;/p&gt;

&lt;p&gt;That's exactly what the next two articles are about.&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt; The concept of agentic QA sounds futuristic, but the tools exist today and the scaffold is built to use them. Next up: how &lt;code&gt;CLAUDE.md&lt;/code&gt; works and why it's the most important file in the project.&lt;/p&gt;

&lt;p&gt;You can find the Public README.md file for the scaffold on GitHub: &lt;a href="https://github.com/idavidov13/Playwright-Scaffold-AI-Assisted-Development-Public" rel="noopener noreferrer"&gt;Playwright Scaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get access to the private GitHub repository here: &lt;a href="https://buymeacoffee.com/idavidov/e/513835" rel="noopener noreferrer"&gt;Get Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Scaffold: Playwright Project Structure Built for AI</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Sun, 01 Mar 2026 06:38:01 +0000</pubDate>
      <link>https://forem.com/idavidov13/the-scaffold-playwright-project-structure-built-for-ai-3a62</link>
      <guid>https://forem.com/idavidov13/the-scaffold-playwright-project-structure-built-for-ai-3a62</guid>
      <description>&lt;p&gt;Have you ever started a new Playwright project and spent the first two days just figuring out where things go? Where do selectors live? How do page objects get into tests? Where does test data belong?&lt;/p&gt;

&lt;p&gt;Most teams answer these questions ad hoc, and end up with a different answer every time. After a few months the codebase looks like that - a mix of conventions, inconsistencies, and copy-pasted patterns that no one quite owns.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;scaffold&lt;/strong&gt; solves this before it starts. But the one we're exploring here was designed with a twist. It wasn't just built for humans. It was built for AI agents to work with.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 What Is a Scaffold?
&lt;/h2&gt;

&lt;p&gt;A scaffold is a pre-built project structure that answers all the "where does this go?" questions upfront. Before you write a single test, you already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A folder for page objects&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A folder for test data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A single import point for all fixtures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Conventions for selectors, naming, and test structure&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like a city grid. Before buildings go up, the streets are laid. You always know how to get from A to B because the plan was made in advance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pages/          → UI page objects and components
tests/          → Test scenarios, organized by area and type
test-data/      → Factories (dynamic) and static JSON (edge cases)
fixtures/       → Dependency injection: wires everything together
enums/          → No hardcoded strings anywhere
config/         → URLs and environment settings
.claude/        → AI instruction files: skills and the orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3ql8qtag8hl7uz264am.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3ql8qtag8hl7uz264am.png" alt="Plan vs Build" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 What the Scaffold Gives You
&lt;/h2&gt;

&lt;p&gt;Beyond folder structure, the scaffold comes with working patterns for every layer of a test suite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Page Object Model:&lt;/strong&gt; UI pages represented as TypeScript classes. Locators and actions in one place. When a selector changes, you update one file. &lt;a href="https://idavidov.eu/building-playwright-framework-step-by-step-implementing-pom-as-fixture-and-auth-user-session" rel="noopener noreferrer"&gt;POM&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fixtures and Dependency Injection:&lt;/strong&gt; Page objects arrive in tests already instantiated. No &lt;code&gt;new LoginPage(page)&lt;/code&gt; boilerplate, no manual setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Type-Safe API Testing:&lt;/strong&gt; Zod - a runtime type validation library - validates API response shapes. If the backend changes a field name, the test fails immediately. &lt;a href="https://idavidov.eu/building-playwright-framework-step-by-step-implementing-api-fixtures" rel="noopener noreferrer"&gt;Zod&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Smart Test Data:&lt;/strong&gt; Faker - a library for generating realistic dummy data - creates unique values every run. Static JSON handles edge cases and invalid inputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Strict Linting:&lt;/strong&gt; ESLint and Prettier enforce conventions automatically. Pre-commit hooks block anything that doesn't meet the standard. If you want to see this in action, the &lt;a href="https://idavidov.eu/never-commit-broken-code-again-a-guide-to-eslint-and-husky-in-playwright" rel="noopener noreferrer"&gt;Never Commit Broken Code Again&lt;/a&gt; article walks through the full setup.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8tajjyodlkyvmjah3pl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8tajjyodlkyvmjah3pl.png" alt="Different Layers" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 But Here's the Real Point
&lt;/h2&gt;

&lt;p&gt;All of that is table stakes for a modern Playwright project. What makes this scaffold different is the &lt;code&gt;.claude/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;On the root level of the project, there is a file called &lt;code&gt;CLAUDE.md&lt;/code&gt; and a &lt;code&gt;.claude/&lt;/code&gt; folder. Inside it lives a separate folder for each skill, along with a file for each one. Together, they define a complete instruction set for an AI agent. Every rule, every pattern, every forbidden anti-pattern is written down in a format that an AI reads before generating a single line of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scaffold was designed so that an AI agent can build and extend it correctly, without supervision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rest of this series is about exactly that: how agentic QA works, what those files contain, and how you can use AI to build a professional test automation framework faster than you thought possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1koxed59lkwtcu25gtp5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1koxed59lkwtcu25gtp5.png" alt="Orchestrator File" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔌 Works With Your Tools
&lt;/h2&gt;

&lt;p&gt;The scaffold isn't locked to a single AI tool. The same rules and skills are provided in three formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; reads &lt;code&gt;CLAUDE.md&lt;/code&gt; and &lt;code&gt;.claude/skills/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; reads &lt;code&gt;.cursor/rules/&lt;/code&gt; and &lt;code&gt;.cursor/skills/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub Copilot&lt;/strong&gt; reads &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; and &lt;code&gt;.github/instructions/&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same architecture, same conventions, same guardrails. You pick the tool you prefer and the scaffold works with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3v6fgkbsq9i5mo7i7qm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3v6fgkbsq9i5mo7i7qm.png" alt="Rules Means Standard" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🐳 One Command to Start
&lt;/h2&gt;

&lt;p&gt;The scaffold ships with a Dev Container configuration. If you have Docker installed, you open the project and everything is ready: Node, Playwright browsers, Python, &lt;code&gt;browser-use&lt;/code&gt;, and AI CLIs. No manual setup, no dependency hunting, no "it works on my machine". One command, fully configured environment.&lt;/p&gt;




&lt;p&gt;🙏🏻 &lt;strong&gt;Thank you for reading!&lt;/strong&gt; This article was the setup. Starting from the next one, we get into the part that changes how you think about test automation. That's the AI side of it. See you there.&lt;/p&gt;

&lt;p&gt;You can find the Public README.md file for the scaffold on GitHub: &lt;a href="https://github.com/idavidov13/Playwright-Scaffold-AI-Assisted-Development-Public" rel="noopener noreferrer"&gt;Playwright Scaffold&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can get access to the private GitHub repository here: &lt;a href="https://buymeacoffee.com/idavidov/e/513835" rel="noopener noreferrer"&gt;Get Access&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>playwright</category>
      <category>ai</category>
    </item>
    <item>
      <title>The AI Shift: Why Specialized Models are the Next Wave for Tech Teams</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Thu, 13 Nov 2025 09:27:10 +0000</pubDate>
      <link>https://forem.com/idavidov13/the-ai-shift-why-specialized-models-are-the-next-wave-for-tech-teams-2n4p</link>
      <guid>https://forem.com/idavidov13/the-ai-shift-why-specialized-models-are-the-next-wave-for-tech-teams-2n4p</guid>
      <description>&lt;p&gt;&lt;strong&gt;A Guide for Developers, QA, and Team Leaders on Moving Beyond General-Purpose AI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the world of software development, new trends hit like waves. And the current AI wave is a tsunami.&lt;/p&gt;

&lt;p&gt;Just like with cloud computing, mobile-first, or Agile, this trend is governed by the classic &lt;strong&gt;Technology Adoption Curve&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;History shows us that a significant portion of professionals (roughly 50%) fall into the &lt;strong&gt;"Late Majority"&lt;/strong&gt; or &lt;strong&gt;"Laggards"&lt;/strong&gt; categories. These are the groups who resist, wait, or just hope the new, &lt;em&gt;disruptive&lt;/em&gt; way of doing things is a temporary fad.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgk0m3c5udlr1iihaokx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flgk0m3c5udlr1iihaokx.png" alt="Inovation Adoption Curve" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But as tech professionals, we know it's never a good career move to ignore the elephant in the room.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;"Innovators"&lt;/strong&gt; and &lt;strong&gt;"Early Adopters"&lt;/strong&gt; understand this. They're already using AI to drive real ROI. They aren't the ones worried about layoffs. Instead they're the ones creating new value.&lt;/p&gt;

&lt;p&gt;But here’s the uncomfortable truth. Just as the Laggards are finally starting to use ChatGPT for basic tasks, the trend is already shifting.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌊 The 5 Groups on the Adoption Curve
&lt;/h2&gt;

&lt;p&gt;To understand where you and your team stand, it helps to know the classic definitions. Every new technology is adopted in this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Innovators (2.5%):&lt;/strong&gt; The visionaries and tinkerers. They are actively building the new tech itself.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early Adopters (13.5%):&lt;/strong&gt; Tech leaders and evangelists who see the potential and are willing to experiment with new tools to gain a competitive edge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Early Majority (34%):&lt;/strong&gt; The practical-minded group. They adopt a new technology once its benefits have been proven by the Early Adopters. This is when the tech "crosses the dip"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Late Majority (34%):&lt;/strong&gt; The skeptics. They only adopt new tech when it's become the new standard, often out of necessity or peer pressure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Laggards (16%):&lt;/strong&gt; The resistors. They are highly resistant to change and are the very last to adopt, often when the old way is no longer supported.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first wave, driven by massive, general-purpose models like GPT-4, Gemini, and Claude, is now being adopted by the Early Majority. But the &lt;em&gt;next&lt;/em&gt; wave is already being built by the Innovators.&lt;/p&gt;




&lt;h2&gt;
  
  
  👑 The King is Dead: The Peak of Giant AI
&lt;/h2&gt;

&lt;p&gt;The "first King" was the &lt;strong&gt;massive, all-purpose model&lt;/strong&gt;. The leap from GPT-2 to GPT-3 was staggering. The leap to GPT-3.5 and 4.0 gave us powerful, human-like chat interfaces that changed everything.&lt;/p&gt;

&lt;p&gt;But now, we're seeing &lt;strong&gt;diminishing returns&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference in practical output between the latest models (like GPT-4 and its successors) is becoming smaller, while the cost to train and run them is exponentially higher. They are fantastic generalists, but they are not specialized masters.&lt;/p&gt;

&lt;p&gt;Think of it this way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A great chef doesn't use a Swiss Army knife to run a world-class kitchen. A Swiss Army knife is a brilliant &lt;em&gt;general&lt;/em&gt; tool, but it can't outperform a specialized sashimi knife, a boning knife, or a paring knife for specific, high-stakes tasks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We are entering the &lt;strong&gt;"Chef's Knife"&lt;/strong&gt; era of AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Long Live the King: The Rise of Specialized AI
&lt;/h2&gt;

&lt;p&gt;The new "King" is &lt;strong&gt;specialization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The future isn't just one giant model trying to do everything. It's a collection of smaller, specific, and hyper-efficient models tailored for precise contexts and needs.&lt;/p&gt;

&lt;p&gt;These specialized tools are built to do one thing perfectly, rather than a million things "pretty well”.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Specialized Models Win
&lt;/h3&gt;

&lt;p&gt;For developer, QA, and leadership workflows, specialized AI offers clear advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;⚡ Peak Performance &amp;amp; Accuracy:&lt;/strong&gt; A model trained &lt;em&gt;only&lt;/em&gt; on your private 2-million-line codebase will always be better at refactoring that code than a general model trained on the public internet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;💰 Lower Cost:&lt;/strong&gt; Running a smaller, focused model is significantly cheaper than paying for API calls to a massive, general-purpose one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;🔒 Enhanced Security &amp;amp; Privacy:&lt;/strong&gt; You can often run these models locally or in your own Virtual Private Cloud (VPC), meaning your proprietary code and sensitive data never leave your control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;💨 Speed:&lt;/strong&gt; Specialized models are optimized for one task, making them faster and less resource-intensive.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Examples of this trend are everywhere:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For Developers:&lt;/strong&gt; AI tools trained specifically on UI/UX best practices to generate front-end code, or assistants fine-tuned on your specific database schema.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For QA:&lt;/strong&gt; Agents designed to generate test cases from your requirements, or models that learn your app's flow to intelligently generate end-to-end test scripts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For Leaders:&lt;/strong&gt; A custom tool that analyzes your team's pull requests and project management data to help predict project bottlenecks before they happen.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏄 How to Catch the Next Wave
&lt;/h2&gt;

&lt;p&gt;I truly believe that AI won't replace all humans.&lt;/p&gt;

&lt;p&gt;But I am &lt;strong&gt;100% sure&lt;/strong&gt; that professionals who understand how to leverage the &lt;strong&gt;right AI for the right job&lt;/strong&gt; will replace those who don't.&lt;/p&gt;

&lt;p&gt;The only way to stay ahead is to get your hands dirty. The key to success is &lt;strong&gt;relentless experimenting and testing&lt;/strong&gt;. A good surfer doesn't waste time watching the wave they just missed. They get busy positioning for the next one.&lt;/p&gt;

&lt;p&gt;Here’s your action plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start Small: Master Your Prompts.&lt;/strong&gt; Treat prompt engineering as a core skill. Don't just ask basic questions. Learn to curate your prompts with deep context, few-shot examples, and specific role-playing. This is the first step from being a &lt;em&gt;consumer&lt;/em&gt; of AI to being a &lt;em&gt;power user&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Go Big: Build Your Own Tools.&lt;/strong&gt; Start thinking about your team's unique problems. What's a repetitive, high-value task that a general AI struggles with? Start designing and implementing specific tools for your team. This could be as simple as a fine-tuned model using an open-source framework or as complex as a custom-built agent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💡 Your Next Move
&lt;/h2&gt;

&lt;p&gt;The "Late Majority" will wait for permission. The "Laggards" will hope it all goes away.&lt;/p&gt;

&lt;p&gt;The winners, the &lt;strong&gt;Early Adopters and Early Majority&lt;/strong&gt;, will be the ones who see this shift happening right now.&lt;/p&gt;

&lt;p&gt;Don't rely on hopes. Do the hard work of experimenting today so you and your team can be the ones receiving the benefits tomorrow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>softwaredevelopment</category>
      <category>qa</category>
    </item>
    <item>
      <title>Developing a Powerful Test Automation Strategy - Frameworks, CI/CD &amp; E2E Tests</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Wed, 15 Oct 2025 06:11:28 +0000</pubDate>
      <link>https://forem.com/idavidov13/developing-a-powerful-test-automation-strategy-frameworks-cicd-e2e-tests-1916</link>
      <guid>https://forem.com/idavidov13/developing-a-powerful-test-automation-strategy-frameworks-cicd-e2e-tests-1916</guid>
      <description>&lt;p&gt;We've built our feature on a solid foundation (&lt;a href="https://idavidov.eu/essential-steps-for-creating-a-robust-software-quality-foundation-culture-roles-and-mindset" rel="noopener noreferrer"&gt;Phase 1&lt;/a&gt;), with a clear blueprint (&lt;a href="https://idavidov.eu/proactive-strategies-for-pre-development-success-requirements-stories-and-planning" rel="noopener noreferrer"&gt;Phase 2&lt;/a&gt;), and with quality implemented in during construction (&lt;a href="https://idavidov.eu/mastering-in-sprint-quality-for-faster-releases-ci-code-reviews-and-collaboration" rel="noopener noreferrer"&gt;Phase 3&lt;/a&gt;). Now, it's time for the final, rigorous inspection before we open the doors to the public.&lt;/p&gt;

&lt;p&gt;Welcome to &lt;strong&gt;Phase 4: Formal Testing &amp;amp; Automation&lt;/strong&gt;. 🤖&lt;/p&gt;

&lt;p&gt;This phase isn't about &lt;em&gt;finding&lt;/em&gt; quality. It's about &lt;em&gt;validating&lt;/em&gt; it at scale. The goal is to build a strategic, automated safety net that provides the team with high confidence before every release. It’s about leveraging technology to ensure the product is not only functional but also stable, performant, and secure.&lt;/p&gt;




&lt;h3&gt;
  
  
  Implementing the End-to-End (E2E) Testing Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A deliberate plan for a &lt;strong&gt;small&lt;/strong&gt; number of automated tests that simulate complete, critical user journeys from start to finish, just as a real user would.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; The goal is to get the &lt;strong&gt;maximum value from the minimum resources&lt;/strong&gt;. E2E tests are powerful, but they are also the most expensive to run and maintain. A strategy of trying to automate everything with E2E tests will quickly lead to a slow, flaky, and unmanageable test suite that no one trusts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with the money:&lt;/strong&gt; Identify the 3-5 user journeys that are most critical to your business. These are often part of the Acceptance Criteria for major features, like the user registration flow, the main checkout process, or creating a core document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a risk-based approach:&lt;/strong&gt; Ask, "What's the most damaging thing that could break?" and add tests for those scenarios. The goal is to have a small suite of E2E tests that, if they pass, give you high confidence that the core business is functional.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodbvt1vcdexxkvdg6r87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodbvt1vcdexxkvdg6r87.png" alt="Maximum Value with Minimum Resources" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Building a Scalable &amp;amp; Maintainable Automation Framework
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The underlying architecture of your test code, designed with the fundamental intention that the application you're testing &lt;strong&gt;will constantly change&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; The number one reason test automation projects fail is because they become a maintenance nightmare. If your tests are brittle and break with every minor UI change, your team will spend more time fixing old tests than writing new ones, and the project will eventually be abandoned.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architect for change:&lt;/strong&gt; The most important principle is the &lt;strong&gt;separation of concerns&lt;/strong&gt;. The test logic (the "what," e.g., "log in") must be separate from the page interactions (the "how," e.g., &lt;code&gt;click('button')&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use design patterns:&lt;/strong&gt; Patterns like the Page Object Model are popular because they enforce this separation. By doing this, if a button's ID changes, you only have to update it in one place, not in 50 different test scripts. This makes your framework resilient and easy to maintain.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabim3if6di1bz2nysdbf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fabim3if6di1bz2nysdbf.png" alt="Architect the Change" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  A Stable &amp;amp; Consistent Test Environment Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A dedicated, production-like environment for running automated tests that is reliable, predictable, and isolated from the chaos of active development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; A flaky test environment makes your test results meaningless. If a test fails, the team must be 99% confident that it's a real bug in the application, not a random glitch in the environment. Without this trust, the entire automation suite loses its value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; This is a &lt;strong&gt;whole-team responsibility&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automate the infrastructure:&lt;/strong&gt; Use Infrastructure as Code (e.g., Terraform, Ansible) to define and deploy your environments so they are consistent every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage the data:&lt;/strong&gt; Have automated processes to refresh the environment with clean, sanitized data on a regular basis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Establish clear rules:&lt;/strong&gt; Define who can deploy to the environment and when, to prevent unexpected changes from derailing test runs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Robust Test Data Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The strategy for ensuring every single automated test has the precise, clean, and isolated data it needs to run successfully, every single time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Garbage in, garbage out. Poor data management is the leading cause of flaky tests. If one test changes a piece of data that another test depends on, you'll be stuck debugging frustrating false negatives.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make tests self-contained:&lt;/strong&gt; The industry best practice is to have tests create their own data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use APIs for setup and teardown:&lt;/strong&gt; Before a test runs, use an API call to create the exact user, product, or state it needs. After the test is finished, use another API call to delete that data. This ensures every test is independent and can be run in any order without side effects.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Integrating Automation into the CI/CD Pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A tiered strategy for running the right tests at the right time to get fast, relevant feedback without slowing down development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Running a 45-minute E2E test suite on every commit would bring development to a halt. A tiered approach intelligently balances the need for speed with the need for confidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Pull Request:&lt;/strong&gt; Run the fastest tests: linters, unit tests, and a small "smoke suite" of critical API checks. The goal is feedback in &lt;strong&gt;under 5 minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Merge to Main Branch:&lt;/strong&gt; Run a larger "regression suite" of integration and UI tests that cover more functionality. The goal is feedback in &lt;strong&gt;under 30 minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nightly/Scheduled:&lt;/strong&gt; Run everything else: the full E2E suite, performance tests, and security scans. This is the final, deep validation that runs when it won't block developers.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qmdl2yrlcmk9irlyfev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qmdl2yrlcmk9irlyfev.png" alt="The Right Test at the Right Time" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Defining a Performance Testing Baseline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The process of measuring and recording your application's performance under a simulated load to establish a "normal" benchmark.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; You can't know if your application is getting slower if you don't know how fast it is today. This baseline is used to detect performance regressions &lt;em&gt;before&lt;/em&gt; your customers complain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick a critical flow:&lt;/strong&gt; Choose something like the API login or a key search query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run a simple load test:&lt;/strong&gt; Use an accessible tool to simulate a realistic load (e.g., 50 users for 5 minutes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure and record:&lt;/strong&gt; Capture the key metrics: &lt;strong&gt;average response time (latency)&lt;/strong&gt; and &lt;strong&gt;requests per second (throughput)&lt;/strong&gt;. This is your baseline. Integrate this test into your nightly build to ensure these numbers don't get worse over time.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Implementing a Basic Security Testing Checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; Integrating automated tools that scan your code (SAST) and your running application (DAST) for common security vulnerabilities listed in resources like the OWASP Top 10.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Security is a critical component of quality. Automatically catching a common vulnerability like &lt;strong&gt;SQL Injection&lt;/strong&gt; or &lt;strong&gt;Cross-Site Scripting (XSS)&lt;/strong&gt; in your pipeline is infinitely cheaper than dealing with a data breach.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integrate free tools:&lt;/strong&gt; Add a tool like OWASP ZAP to your CI/CD pipeline (the nightly build is a great place for it). This provides a valuable first layer of defense and can catch low-hanging fruit without needing a dedicated security expert.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Cross-Browser &amp;amp; Cross-Device Testing Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A deliberate plan, based on real user data, for ensuring your application works correctly on the browsers and devices your customers actually use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Developers often work exclusively in one browser. This can easily lead to CSS or JavaScript bugs that break the experience for a significant portion of your user base on other browsers or mobile devices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Let data drive your decisions:&lt;/strong&gt; This should be decided upfront based on &lt;strong&gt;customer needs&lt;/strong&gt;. Use your analytics tools (like Google Analytics) to identify the top browsers, operating systems, and screen sizes that represent 90%+ of your traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus your efforts:&lt;/strong&gt; Prioritize your testing on that specific set of configurations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use cloud services for scale:&lt;/strong&gt; Leverage a service like BrowserStack or Sauce Labs to run your automated tests across all your target configurations in parallel, giving you broad coverage without a massive time investment.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Conclusion: Building Unshakeable Confidence
&lt;/h3&gt;

&lt;p&gt;Phase 4 is about building unshakeable confidence in your product through smart, strategic validation. This automated safety net doesn't just catch bugs. It allows your team to develop and release with greater speed and less fear.&lt;/p&gt;

&lt;p&gt;With our product now thoroughly inspected and secured, it's time for the moment of truth. In our final article, we'll explore &lt;strong&gt;Phase 5: Release &amp;amp; Post-Release&lt;/strong&gt;, where our software meets the real world and our quality journey continues.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>softwaredevelopment</category>
      <category>software</category>
    </item>
    <item>
      <title>Mastering In-Sprint Quality for Faster Releases - CI, Code Reviews &amp; Collaboration</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Fri, 10 Oct 2025 12:04:49 +0000</pubDate>
      <link>https://forem.com/idavidov13/mastering-in-sprint-quality-for-faster-releases-ci-code-reviews-collaboration-4pmd</link>
      <guid>https://forem.com/idavidov13/mastering-in-sprint-quality-for-faster-releases-ci-code-reviews-collaboration-4pmd</guid>
      <description>&lt;p&gt;We have our cultural foundation &lt;a href="https://idavidov.eu/essential-steps-for-creating-a-robust-software-quality-foundation-culture-roles-and-mindset" rel="noopener noreferrer"&gt;Essential Steps for Creating a Robust Software Quality Foundation - Culture, Roles &amp;amp; Mindset&lt;/a&gt; and our architectural blueprints &lt;a href="https://idavidov.eu/proactive-strategies-for-pre-development-success-requirements-stories-and-planning" rel="noopener noreferrer"&gt;Proactive Strategies for Pre-Development Success - Requirements, Stories &amp;amp; Planning&lt;/a&gt;. Now, it's time to pick up the tools and start building. Welcome to the construction site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: In-Development Quality&lt;/strong&gt; is all about the practices that happen &lt;em&gt;during&lt;/em&gt; a sprint. This isn't a phase that happens at the end. Instead it's the engine room of quality, running continuously. The core principles here are &lt;strong&gt;fast feedback loops&lt;/strong&gt; and &lt;strong&gt;deep collaboration&lt;/strong&gt;. These are the activities that ensure quality is built directly into the product as it's being assembled.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Fast and Reliable Continuous Integration (CI) Pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A CI pipeline is an automated process, triggered on every code change, that builds the software and runs a suite of automated tests to ensure the change didn't break anything. It's the team's first line of defense.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; The value of a CI pipeline is directly tied to its speed and reliability.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow Feedback Kills Momentum:&lt;/strong&gt; If a developer has to wait 30 minutes for feedback, they've already moved on to another task. When the failure notification finally arrives, they've lost all mental context, making the fix five times harder. A fast pipeline (under 10 minutes) provides immediate feedback while the code is still fresh in their mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flaky Tests Destroy Trust:&lt;/strong&gt; A flaky pipeline that fails randomly is worse than no pipeline at all. When the team can't trust the results, they start ignoring failures. This completely defeats the purpose of having an automated safety net.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;HOW to achieve it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallelize your tests:&lt;/strong&gt; Run different suites of tests simultaneously to drastically cut down execution time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimize your build:&lt;/strong&gt; Use caching for dependencies so you aren't downloading the internet on every run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce a zero-tolerance flaky test policy:&lt;/strong&gt; If a test is flaky, it's a bug. Immediately quarantine it or fix it.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4m2zu8fcniwcpsuvun7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4m2zu8fcniwcpsuvun7.png" alt="Fast CI" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Enforced Code Quality Standards &amp;amp; Static Analysis
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; Using automated tools, known as linters (like ESLint for JavaScript), to automatically scan code for stylistic errors, potential bugs, and adherence to team-defined standards.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; This is about cognitive load. Humans are slow, inconsistent, and error-prone when performing repetitive, detail-oriented tasks like checking for correct indentation or unused variables. Offloading this work to a machine frees up precious human brainpower during code reviews to focus on what truly matters: the logic, the architecture, and the user experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Integrate into the editor:&lt;/strong&gt; Configure the linter to run directly in every developer's code editor, providing instant, private feedback as they type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it a required check:&lt;/strong&gt; Enforce the standards by making the linting step a mandatory pass/fail check before every commit. This ensures no non-compliant code ever makes it into the repository.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Mandatory, Effective Peer Reviews (Pull Requests)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The practice where at least one other team member must thoughtfully review and approve a developer's code changes before they can be merged into the main codebase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; While finding bugs is a benefit, the primary goal of a code review is &lt;strong&gt;knowledge sharing&lt;/strong&gt;. It's the single most effective way to spread context throughout the team, prevent knowledge silos, improve code consistency, and collectively elevate the team's skills.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to make them effective:&lt;/strong&gt; The tone and focus of the comments are everything. A good review is a dialogue, not a judgment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;❌ Bad Comment:&lt;/strong&gt; "This is inefficient. Use a hash map." (A command)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✅ Good Comment:&lt;/strong&gt; "This is an interesting approach. I'm wondering if a hash map might be more performant here for the lookup. What are your thoughts on that?" (A collaborative question)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fetgtkgapvmfl39f9ax4w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fetgtkgapvmfl39f9ax4w.png" alt="Good Pull Request" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  In-Sprint Dev-QA Collaboration &amp;amp; Pairing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; Direct, real-time collaboration between developers and QAs throughout the lifecycle of a feature, most powerfully realized through &lt;strong&gt;pair-testing&lt;/strong&gt; sessions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; This practice demolishes the "us vs. them" wall. It creates a tight, immediate feedback loop that resolves ambiguities in minutes instead of days. It fosters a powerful sense of shared ownership over the quality of the feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; The pair-testing session is the key ritual.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schedule a short meeting:&lt;/strong&gt; Book a 15-minute session once the developer has a working version of the feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev demonstrates:&lt;/strong&gt; The developer shares their screen and explains &lt;em&gt;what&lt;/em&gt; they built and &lt;em&gt;how&lt;/em&gt; it's implemented. This is a mini knowledge-transfer session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test together:&lt;/strong&gt; Both the dev and QA perform a few key tests together, discussing the results in real-time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handoff:&lt;/strong&gt; This session brings instant clarity and serves as the perfect handoff. The QA can then take over to perform deeper, more comprehensive exploratory testing, already armed with full context.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1987cgbaisprensjd8tp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1987cgbaisprensjd8tp.png" alt="Collaboration" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Early and Continuous Exploratory Testing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A creative, unscripted, and human-driven approach to testing. It's a "tour" of the feature where the tester uses their knowledge, curiosity, and intuition to discover how the software actually behaves. It’s about investigation and learning, not just script execution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Automation is excellent at &lt;em&gt;verifying&lt;/em&gt; that the software does what you expect. Exploratory testing is essential for &lt;em&gt;discovering&lt;/em&gt; what the software does when you do something unexpected. It allows you to &lt;code&gt;feel&lt;/code&gt; the application, uncovering usability flaws, confusing workflows, and complex state-related bugs that automation will always miss.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start early:&lt;/strong&gt; As soon as a piece of functionality is available, start exploring it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use charters:&lt;/strong&gt; Give your testing a mission. Instead of just randomly clicking, create a goal like, "Investigate how the application handles a user losing their network connection while updating their profile."&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Definition of "Ready for QA/Review"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A simple checklist, formally part of the team's Definition of Done (DoD), that confirms a piece of work is truly ready for deeper testing and review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; This checklist prevents the frustrating "ping-pong" where a ticket is passed back and forth because of missing information, a broken build, or unmet requirements. It creates a smooth, efficient, and respectful handoff process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to implement it:&lt;/strong&gt; This checklist is the final gate before the pair-testing session. It should live as a template in your tickets or pull requests and include items like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ All Acceptance Criteria have been implemented.&lt;/li&gt;
&lt;li&gt;✅ The CI pipeline is green.&lt;/li&gt;
&lt;li&gt;✅ The feature is deployed to the shared testing environment.&lt;/li&gt;
&lt;li&gt;✅ A 15-minute pair-testing session has been scheduled.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb23c3mhtdlbd8084v94j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb23c3mhtdlbd8084v94j.png" alt="Blueprint" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion: The Engine of Quality
&lt;/h3&gt;

&lt;p&gt;Phase 3 is the engine room where the blueprints from Phase 2 become a tangible, working product. Powered by the fast feedback of CI and the deep collaboration of pairing and code reviews, these in-sprint practices are what transform a plan into high-quality, functional software.&lt;/p&gt;

&lt;p&gt;Now that we've built the feature with quality baked in, it's time to validate our confidence at scale. In the next article, we'll dive into &lt;strong&gt;Phase 4: Formal Testing &amp;amp; Automation&lt;/strong&gt;, where we'll build the strategic, automated safety net that protects our product and our users.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>qa</category>
      <category>softwaredevelopment</category>
      <category>software</category>
    </item>
    <item>
      <title>Proactive Strategies for Pre-Development Success - Requirements, Stories &amp; Planning</title>
      <dc:creator>idavidov13</dc:creator>
      <pubDate>Thu, 02 Oct 2025 05:17:55 +0000</pubDate>
      <link>https://forem.com/idavidov13/proactive-strategies-for-pre-development-success-requirements-stories-planning-2gom</link>
      <guid>https://forem.com/idavidov13/proactive-strategies-for-pre-development-success-requirements-stories-planning-2gom</guid>
      <description>&lt;p&gt;In our previous article &lt;a href="https://idavidov.eu/essential-steps-for-creating-a-robust-software-quality-foundation-culture-roles-and-mindset" rel="noopener noreferrer"&gt;Essential Steps for Creating a Robust Software Quality Foundation - Culture, Roles &amp;amp; Mindset&lt;/a&gt;, we laid the cultural foundation for quality. We established that quality is a shared mindset, not just a department. But a strong foundation is useless without a solid plan. A nothing huge is built on hopes and good intentions. Instead it's built from a detailed architectural blueprint.&lt;/p&gt;

&lt;p&gt;Welcome to &lt;strong&gt;Phase 2: Pre-Development&lt;/strong&gt;. This is the architect's table.&lt;/p&gt;

&lt;p&gt;This phase is about designing quality into your product from the very start. Every activity here is designed to prevent entire classes of bugs before a single line of code is written. This is where an hour of careful planning saves ten hours of painful debugging later. Let's dive into it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtf8kvabw29klte9hj4h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqtf8kvabw29klte9hj4h.png" alt="Blueprint" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Crafting High-Quality, Testable User Stories
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A high-quality user story clearly and concisely describes a feature from an end-user's perspective. It's not a technical task list, but a statement of user value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Vague stories are the root of most misunderstandings. They force developers and QAs to make assumptions, which leads to building the wrong thing, extensive rework, and features that completely miss the user's needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; You can use the standard BDD (Behavior Driven Development) style &lt;code&gt;As a [persona], I want [action], so that I can [achieve an outcome]&lt;/code&gt; format. This structure forces clarity. Now, compare a bad story to a good one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;❌ Bad:&lt;/strong&gt; "User login"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✅ Good:&lt;/strong&gt; "As a &lt;strong&gt;registered user&lt;/strong&gt;, I want to &lt;strong&gt;log in with my email and password&lt;/strong&gt; so that I can &lt;strong&gt;access my account dashboard&lt;/strong&gt;."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second version is instantly testable and leaves no room for guessing who the user is or what the goal is. A good approach is to use the &lt;strong&gt;INVEST&lt;/strong&gt; criteria (Independent, Negotiable, Valuable, Estimable, Small, Testable) as a checklist for every story.&lt;/p&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Defining Concrete Acceptance Criteria (ACs)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT they are:&lt;/strong&gt; Acceptance Criteria are a set of specific, binary (pass/fail) conditions that a user story must meet to be considered "done". They are the formal contract for the feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY they matter:&lt;/strong&gt; The greatest strength of binary ACs is that they &lt;strong&gt;eliminate assumptions&lt;/strong&gt;. When an AC is subjective ("The page should load fast"), it leads to arguments. When it's binary ("The page must load in under 2 seconds on a 4G connection"), it's a simple, verifiable fact. This robustness is key to shipping with confidence.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to write them:&lt;/strong&gt; Focus on clear, testable outcomes. While a format like Gherkin (&lt;code&gt;Given/When/Then&lt;/code&gt;) can be useful, the principle is more important than the syntax.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;❌ Bad:&lt;/strong&gt; "The login process should be user-friendly." (Subjective)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✅ Good:&lt;/strong&gt; "When the user enters an incorrect password, the error message 'Invalid credentials' is displayed." (Pass/Fail)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;✅ Good:&lt;/strong&gt; "Upon successful login, the user is redirected to the &lt;code&gt;/dashboard&lt;/code&gt; page." (Pass/Fail)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Implementing "Three Amigos" Sessions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A focused working session bringing together the three key perspectives: &lt;strong&gt;Product&lt;/strong&gt; (what is the problem to solve?), &lt;strong&gt;Development&lt;/strong&gt; (how can we build a solution?), and &lt;strong&gt;Quality&lt;/strong&gt; (how could this solution break?).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; This session is the ultimate defense against ambiguity. It demolishes silos and ensures a shared understanding of the feature, its requirements, and its risks &lt;em&gt;before&lt;/em&gt; the first line of code is written. It’s a high-leverage activity that prevents countless hours of wasted work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to run it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep it small:&lt;/strong&gt; It's the "Three Amigos", not the "Thirty Amigos". Only the three core perspectives should be present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have a strict agenda:&lt;/strong&gt; Start with the initial user story requirements and ACs as a foundation. The goal is to brainstorm the story requirements and to set ACs foundation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define responsibilities:&lt;/strong&gt; The Product representative clarifies requirements, the Developer discusses implementation strategy, and the QA probes for edge cases and testability. The goal is to leave the session with crystal-clear, agreed-upon ACs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcyifziym3263vwumrw1p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcyifziym3263vwumrw1p.png" alt="Three Amigos" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Identifying Edge Cases &amp;amp; Negative Paths Upfront
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The practice of systematically thinking through what happens when things go wrong or when users behave unexpectedly (e.g., entering bad data, losing network, clicking things out of order). This is a critical activity for &lt;strong&gt;story grooming sessions&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Thinking about negative paths is a core "shift-left" activity. Uncovering a major edge case in a 15-minute grooming session is infinitely cheaper and faster than discovering it through a production bug that impacts thousands of users. This is where a QA's mindset adds immense value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; The QA should lead this by asking probing "What if...?" questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What if the user tries to register with an email address that already exists?"&lt;/li&gt;
&lt;li&gt;"What if the API call fails while they are submitting the form?"&lt;/li&gt;
&lt;li&gt;"What if the user's session times out while they have items in their cart?"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Defining and Integrating Non-Functional Requirements (NFRs)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT they are:&lt;/strong&gt; NFRs define &lt;em&gt;how&lt;/em&gt; the system should operate, rather than &lt;em&gt;what&lt;/em&gt; it should do. The most common examples are &lt;strong&gt;performance, security, and accessibility&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; Leaving NFRs until the end is a recipe for disaster. You can't "add" security or performance to a product after it's built. It has to be designed in. Thinking about NFRs while planning the functional story is crucial because you have the &lt;strong&gt;full context&lt;/strong&gt; to make smart architectural decisions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; NFRs can be tracked as their own stories, but they must be discussed alongside the related functional story. For example, while grooming a "File Upload" story, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; "What is the maximum file size we need to support? What's the target upload time?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; "What file types are allowed? How will we scan for viruses?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility:&lt;/strong&gt; "How will a screen reader announce the upload progress?"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h3&gt;
  
  
  Risk-Based Prioritization of Quality Efforts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; A strategy to focus your most intense testing efforts on the areas of the application that pose the greatest risk to the business and your users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; You can't test everything with 100% depth, since it's not practical or economical. This approach ensures your limited time is spent where it matters most. It’s about being effective, not just busy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; Use a simple &lt;strong&gt;Impact vs. Likelihood&lt;/strong&gt; matrix to categorize features and guide your testing strategy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High Impact / High Likelihood&lt;/strong&gt; (e.g., the main payment flow): This requires deep, rigorous testing, including end-to-end automation and extensive exploratory testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Impact / Low Likelihood&lt;/strong&gt; (e.g., data restoration from a backup): This needs a clear test plan for key scenarios but doesn't require exhaustive daily testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Impact / High Likelihood&lt;/strong&gt; (e.g., a form validation error): This is a perfect candidate for lightweight, automated checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Impact / Low Likelihood&lt;/strong&gt; (e.g., updating a profile's bio): This can be covered sufficiently by quick exploratory testing.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q7uzk9okz0feqky1kzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q7uzk9okz0feqky1kzl.png" alt="Risk-Based Prioritization" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Making Quality a Factor in Story Estimation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The practice of including all quality-related activities (writing unit tests, developing automation test scripts, pair testing, exploratory testing, etc.) within the story point estimation for every user story.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; &lt;strong&gt;Testing is not a separate phase, It's part of the development work.&lt;/strong&gt; Creating separate "testing tickets" breaks the "Whole Team" mentality and hides the true cost of delivering a feature. Including it in the estimate makes it clear that a story isn't "done" until it's "done and high-quality".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; During estimation, the team must explicitly discuss the effort required for quality. A developer might say a story is "3 points", and the QA can then ask, "Does that estimate include the time to write the new automated tests and for us to do a 30-minute exploratory session together?" This conversation ensures the full scope of work is accounted for.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Early Test Data &amp;amp; Environment Planning
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHAT it is:&lt;/strong&gt; The strategic process of defining the specific data and infrastructure needed to properly test a feature, and doing so &lt;em&gt;before&lt;/em&gt; development begins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WHY it matters:&lt;/strong&gt; "Blocked by test data" or "The test environment is broken" are two of the most common and frustrating bottlenecks in the development cycle. They are almost always preventable with upfront planning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;HOW to do it:&lt;/strong&gt; Before a story is considered "ready for development", the team must be able to answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data:&lt;/strong&gt; What specific data states do we need? (e.g., A brand new user? A user with 1000+ orders? A user with an expired subscription? A user in a different country?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment:&lt;/strong&gt; Does this feature depend on a new third-party service or a change in our infrastructure? If so, how will we make that available for testing?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lbwiqt6kw9xgv57a7lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lbwiqt6kw9xgv57a7lr.png" alt="Pre-Development Quality" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Conclusion: From Foundation to Framework
&lt;/h3&gt;

&lt;p&gt;Phase 2 transforms the cultural values from Phase 1 into a concrete plan of action. By focusing on clarity, collaboration, and risk mitigation &lt;em&gt;before&lt;/em&gt; development, you build a sturdy framework that prevents defects and ensures you're building the right product, the right way.&lt;/p&gt;

&lt;p&gt;With the blueprints now finalized, it's time to pick up the tools and start the construction. In our next article, we'll dive into &lt;strong&gt;Phase 3: In-Development Quality&lt;/strong&gt;, where we'll explore the in-sprint practices that ensure quality is built in, not bolted on.&lt;/p&gt;

</description>
      <category>qa</category>
      <category>testing</category>
      <category>software</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
