<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>ProvaLab Blog</title>
    <link>https://provalab.io/blog/</link>
    <description>Testing, security and load testing for teams that write code with AI agents.</description>
    <language>en</language>
    <atom:link href="https://provalab.io/feed.xml" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Fri, 30 Oct 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>Your first load test with k6 (hands-on): find where your API slows down</title>
      <link>https://provalab.io/blog/first-load-test-k6/</link>
      <guid isPermaLink="true">https://provalab.io/blog/first-load-test-k6/</guid>
      <pubDate>Fri, 30 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>load-testing</category>
      <category>k6</category>
      <category>performance-testing</category>
      <description>Write and run a k6 load test against the API from Part 4, set pass/fail thresholds, read the summary, then run a stress test to 1,000 users and find where it slows, with the real output.</description>
      <content:encoded><![CDATA[<p><a href="https://provalab.io/blog/performance-testing-types/">Performance testing types</a> explained load, stress, spike and soak tests. This final post of the Hands-On series runs two of them for real, with <strong>k6</strong>, against the discount API from <a href="https://provalab.io/blog/first-api-test-pytest/">Part 4</a>:</p>
<ol>
<li>A <strong>load test</strong> at the expected peak: does it hold up?</li>
<li>A <strong>stress test</strong> past the peak: where does it slow down, and does it break?</li>
</ol>
<p>Every command and every line of output below was run while writing this post.</p>
<p><strong>Before you start:</strong> a load test is, by definition, a flood of traffic. <strong>Only ever point it at something you own</strong>, and never at production without a plan. Our target is the API from Part 4, running on our own machine.</p>
<h2>Step 1: Get k6 (1 minute)</h2>
<p>k6 is a single program. Install it from <a href="https://k6.io">k6.io</a> for your operating system, or run it from its container image, which is what we do here:</p>
<pre><code class="language-bash">docker pull grafana/k6:latest
docker run --rm grafana/k6:latest version
</code></pre>
<pre><code class="language-text">k6 v2.3.0 (commit/e088784614, go1.27.1, linux/amd64)
</code></pre>
<h2>Step 2: Start the target (1 minute)</h2>
<p>In the Part 4 folder, with the fixed version of the API from <a href="https://provalab.io/blog/first-security-scan-owasp-zap/">Part 6</a>:</p>
<pre><code class="language-bash">uvicorn app:app
</code></pre>
<p>One request by hand, to know what &quot;normal&quot; looks like before we add load:</p>
<pre><code class="language-bash">curl -s -o /dev/null -w &quot;%{http_code} in %{time_total}s\n&quot; -X POST http://127.0.0.1:8000/discounts/apply \
  -H &quot;Authorization: Bearer token-alice&quot; -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;}&#39;
</code></pre>
<pre><code class="language-text">200 in 0.039147s
</code></pre>
<p>Forty milliseconds for one request, on an idle machine. The question load testing answers is: what happens to that number when it&#39;s fifty requests at once?</p>
<h2>Step 3: Write the load test (4 minutes)</h2>
<p>Create a folder called <code>k6</code> and in it <code>load.js</code>:</p>
<pre><code class="language-javascript">import http from &quot;k6/http&quot;;
import { check, sleep } from &quot;k6&quot;;

const BASE = __ENV.API_URL || &quot;http://127.0.0.1:8000&quot;;
const HEADERS = { Authorization: &quot;Bearer token-alice&quot;, &quot;Content-Type&quot;: &quot;application/json&quot; };

export const options = {
  stages: [
    { duration: &quot;30s&quot;, target: 50 }, // ramp up to 50 users
    { duration: &quot;60s&quot;, target: 50 }, // hold the expected peak
    { duration: &quot;15s&quot;, target: 0 },  // ramp down
  ],
  thresholds: {
    http_req_duration: [&quot;p(95)&lt;200&quot;], // 95% of requests under 200 ms
    http_req_failed: [&quot;rate&lt;0.01&quot;],   // less than 1% errors
  },
};

export default function () {
  // 7 in 10 users apply a discount, 3 in 10 look at an order
  if (Math.random() &lt; 0.7) {
    const res = http.post(`${BASE}/discounts/apply`, JSON.stringify({ order_total: 500, code: &quot;SAVE10&quot; }), { headers: HEADERS });
    check(res, { &quot;discount applied&quot;: (r) =&gt; r.status === 200 &amp;&amp; r.json(&quot;total&quot;) === 450 });
  } else {
    const res = http.get(`${BASE}/orders/42`, { headers: HEADERS });
    check(res, { &quot;order returned&quot;: (r) =&gt; r.status === 200 });
  }
  sleep(1); // think time: a real user pauses between actions
}
</code></pre>
<p>The four ideas in this file are the four ideas in any load test:</p>
<ul>
<li><strong>Stages</strong> describe the shape of the load over time: ramp up, hold, ramp down. The ramp matters; starting 50 users in the same millisecond tests something different (that&#39;s a spike test).</li>
<li><strong>Thresholds</strong> are the pass/fail rules, decided <em>before</em> running: p95 under 200 ms, under 1% errors. Without them you get numbers but no verdict.</li>
<li><strong>The default function</strong> is what one <strong>virtual user (VU)</strong> does, over and over: here, a realistic mix of two actions.</li>
<li><strong>Think time</strong> (<code>sleep(1)</code>) is the pause a real person takes between actions. Without it, 50 virtual users behave like thousands of real ones.</li>
</ul>
<p>The <code>check</code> lines are the load test&#39;s assertions: a <code>200</code> with the right total. A load test that only counts responses would happily report success while every response was an error page.</p>
<h2>Step 4: Run it (2 minutes)</h2>
<pre><code class="language-bash">docker run --rm --network host -e API_URL=http://127.0.0.1:8000 -v &quot;$PWD/k6:/scripts&quot; \
  grafana/k6:latest run /scripts/load.js
</code></pre>
<p>(As in Part 6, on macOS and Windows use <code>http://host.docker.internal:8000</code> and drop <code>--network host</code>.)</p>
<p>k6 prints a progress line every second, then the summary:</p>
<pre><code class="language-text">  █ TOTAL RESULTS

    checks_total.......: 4145    39.338342/s
    checks_succeeded...: 100.00% 4145 out of 4145
    checks_failed......: 0.00%   0 out of 4145

    ✓ discount applied
    ✓ order returned

    HTTP
    http_req_duration..............: avg=1.91ms min=975.39µs med=1.68ms max=16.09ms p(90)=2.7ms p(95)=3.15ms
    http_req_failed................: 0.00%  0 out of 4145
    http_reqs......................: 4145   39.338342/s

    EXECUTION
    iteration_duration.............: avg=1s     min=1s       med=1s     max=1.01s   p(90)=1s    p(95)=1s
    iterations.....................: 4145   39.338342/s
    vus............................: 1      min=1         max=50
    vus_max........................: 50     min=50        max=50

default ✓ [ 100% ] 00/50 VUs  1m45s
</code></pre>
<p><strong>Passed.</strong> 4,145 requests over 1 minute 45, every check green, no failures, and a p95 of <strong>3.15 ms</strong> against a target of 200 ms. At its expected peak this API isn&#39;t remotely stressed. (The &quot;iteration_duration&quot; of 1 s is just the think time.)</p>
<p>Note the tick next to <code>default</code> and the absence of any threshold error: that&#39;s k6&#39;s verdict. In CI, a crossed threshold makes k6 exit with a non-zero code, which fails the job.</p>
<h2>Step 5: Stress it (5 minutes)</h2>
<p>Passing at 50 users tells you it&#39;s fine today. It doesn&#39;t tell you how much headroom you have, or what failure looks like. For that, keep increasing the load past the expected peak until something gives. Create <code>stress.js</code>:</p>
<pre><code class="language-javascript">import http from &quot;k6/http&quot;;
import { check } from &quot;k6&quot;;

const BASE = __ENV.API_URL || &quot;http://127.0.0.1:8000&quot;;
const HEADERS = { Authorization: &quot;Bearer token-alice&quot;, &quot;Content-Type&quot;: &quot;application/json&quot; };

export const options = {
  stages: [
    { duration: &quot;20s&quot;, target: 100 },
    { duration: &quot;20s&quot;, target: 300 },
    { duration: &quot;20s&quot;, target: 600 },
    { duration: &quot;20s&quot;, target: 1000 }, // keep going past the expected peak
    { duration: &quot;10s&quot;, target: 0 },
  ],
  thresholds: {
    // we expect these to fail: the point is to see where
    http_req_duration: [&quot;p(95)&lt;500&quot;],
    http_req_failed: [&quot;rate&lt;0.05&quot;],
  },
};

export default function () {
  const res = http.post(`${BASE}/discounts/apply`, JSON.stringify({ order_total: 500, code: &quot;SAVE10&quot; }), { headers: HEADERS });
  check(res, { &quot;status 200&quot;: (r) =&gt; r.status === 200 });
  // no think time: every virtual user hammers the API as fast as it can
}
</code></pre>
<p>Two deliberate changes: the load climbs in steps to 1,000 users, and there&#39;s <strong>no think time</strong>, so each virtual user sends requests as fast as the API answers. Run it the same way, with <code>stress.js</code>:</p>
<pre><code class="language-text">  █ TOTAL RESULTS

    checks_total.......: 70982   788.671898/s
    checks_succeeded...: 100.00% 70982 out of 70982
    checks_failed......: 0.00%   0 out of 70982

    ✓ status 200

    HTTP
    http_req_duration..............: avg=498.54ms min=731.6µs med=430.55ms max=1.79s p(90)=1.1s p(95)=1.51s
    http_req_failed................: 0.00% 0 out of 70982
    http_reqs......................: 70982 788.671898/s

    EXECUTION
    vus............................: 13    min=5          max=997
    vus_max........................: 1000  min=1000       max=1000

default ✓ [ 100% ] 0000/1000 VUs  1m30s
time=&quot;2026-10-29T18:31:28Z&quot; level=error msg=&quot;thresholds on metrics &#39;http_req_duration&#39; have been crossed&quot;
</code></pre>
<p><img src="https://provalab.io/blog/handson/read-k6-summary.svg" alt="Reading a k6 summary. checks_succeeded 100%: every response had the right status. http_req_duration: read p(95), not avg; here 1.51 s, meaning 1 in 20 waited that long or more. http_req_failed 0%: nothing errored outright. http_reqs 70,982 at 789 per second: the throughput the API sustained. vus_max 1000: how many users were hitting it. The threshold line at the bottom is the verdict: failed on speed, not on errors." width="880" height="440" loading="lazy"></p>
<p>Read it line by line:</p>
<ul>
<li><strong>70,982 requests in 90 seconds, 789 per second.</strong> Twenty times the load test&#39;s rate.</li>
<li><strong>Still 0% failed, every check green.</strong> The API never fell over. Every one of 70,982 requests got a correct <code>200</code>.</li>
<li><strong>But p95 is 1.51 s</strong>, and the maximum 1.79 s. One in twenty customers would wait a second and a half for a discount to apply. That&#39;s the threshold k6 crossed, and the reason the run is marked failed.</li>
</ul>
<p>So the honest answer from this stress test is: <strong>this API doesn&#39;t break under 1,000 hammering users; it slows down</strong>, from 3 ms to 1.5 s at the 95th percentile. That&#39;s the &quot;breaking point&quot; for a single-process server: the point where the queue of waiting requests grows faster than it drains. Whether that&#39;s acceptable depends on your product, which is exactly why thresholds are written down before the run.</p>
<p><img src="https://provalab.io/blog/handson/load-vs-stress-results.svg" alt="Two runs side by side. Load test: 50 users with think time, ramp 30 s, hold 60 s, ramp down 15 s; 4,145 requests, 0 errors, p95 3.15 ms, passed. Stress test: steps to 100, 300, 600 and 1,000 users with no think time; 70,982 requests at 789 per second, still 0 errors, p95 1.51 s, threshold crossed: it slowed, it didn't break." width="880" height="470" loading="lazy"></p>
<h2>What we&#39;d do next</h2>
<ul>
<li><strong>Find the knee.</strong> Re-run with finer steps (200, 250, 300…) to find the user count where p95 first crosses 500 ms. That number is your capacity, and it belongs in the <a href="https://provalab.io/blog/how-to-write-a-test-plan/">test plan</a>.</li>
<li><strong>Watch the server, not just k6.</strong> CPU, memory and, for a real app, the database. Here the limit is one Python process on one core; <code>uvicorn --workers 4</code> would roughly quadruple it. Load testing tells you <em>that</em> it slows; server metrics tell you <em>why</em>.</li>
<li><strong>Run it from a separate machine.</strong> We ran k6 on the same laptop as the API, so they competed for CPU. For real numbers the load generator must not share resources with the target, and for big loads it must be several machines.</li>
<li><strong>Add a soak run:</strong> 50 users for two hours, to catch memory leaks a 90-second test never will.</li>
</ul>
<h2>Putting it in the pipeline</h2>
<p>Nightly, not on every commit (a load test takes minutes and needs a quiet environment):</p>
<pre><code class="language-yaml">      - run: uvicorn app:app &amp;
      - run: &gt;
          docker run --rm --network host -e API_URL=http://127.0.0.1:8000
          -v &quot;$PWD/k6:/scripts&quot; grafana/k6:latest run /scripts/load.js
</code></pre>
<p>If the thresholds are crossed, k6 exits non-zero and the job goes red. Over months, that catches the commit that made things slower.</p>
<h2>Habits worth keeping</h2>
<ol>
<li><strong>Decide the thresholds before you run.</strong> Numbers without a target are trivia.</li>
<li><strong>Read p95 and p99, never the average.</strong> The average in the stress run (498 ms) hid a p95 three times worse.</li>
<li><strong>Assert on the response</strong>, not just the status. A fast wrong answer is still wrong.</li>
<li><strong>Model real behaviour:</strong> a mix of actions, with think time. Drop the think time only when you mean to stress.</li>
<li><strong>Never load-test what you don&#39;t own.</strong> And never production without warning the people who run it.</li>
</ol>
<h2>Hands-On Testing: the complete series</h2>
<p>That closes the series. Over seven parts you&#39;ve built and run:</p>
<ol>
<li><a href="https://provalab.io/blog/ui-testing-explained/">UI testing explained</a> · 2. <a href="https://provalab.io/blog/first-playwright-test/">Your first Playwright test</a></li>
<li><a href="https://provalab.io/blog/api-testing-explained/">API testing explained</a> · 4. <a href="https://provalab.io/blog/first-api-test-pytest/">Your first API tests</a></li>
<li><a href="https://provalab.io/blog/security-testing-basics-owasp-top-10/">Security testing basics: the OWASP Top 10</a> · 6. <a href="https://provalab.io/blog/first-security-scan-owasp-zap/">Your first security scan with OWASP ZAP</a></li>
<li>Your first load test with k6 (this post)</li>
</ol>
<p>Together with the 23-part <a href="https://provalab.io/blog/what-is-software-testing/">Testing Basics</a> series, that&#39;s a complete foundation: the ideas, and the tools in your hands.</p>
<p>ProvaLab&#39;s load testing runs k6 tests like these from the cloud, on machines that don&#39;t compete with your app, shows p95 and throughput live, and can find the user count where your API first slows down, step by step. Your coding agent can write the script from your code and explain the slow spots afterwards. See it in the <a href="https://provalab.io/#stage-load">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Playwright with Python in practice: fixtures, page objects, saved state, mocked APIs and parallel runs</title>
      <link>https://provalab.io/blog/playwright-python-page-objects-fixtures-mocking/</link>
      <guid isPermaLink="true">https://provalab.io/blog/playwright-python-page-objects-fixtures-mocking/</guid>
      <pubDate>Fri, 30 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>playwright</category>
      <category>pytest</category>
      <category>test-automation</category>
      <category>tool-guide</category>
      <description>Hands-on: grow three Playwright tests into a real suite with page objects, pytest fixtures, saved browser state, an API mocked with page.route, and a parallel run that more than halves the time.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/first-playwright-test/">Hands-On Part 2</a> we wrote three Playwright tests in one file, ran them against this site and read a failure. That is where most tutorials stop, and where most real suites start to hurt: the fourth file copies the first, every test dismisses the same cookie banner, one test submits a real form and creates a real record in the real database, and the whole thing takes a minute because nothing runs in parallel.</p>
<p>This guide takes the three tests from Part 2 and grows them into a suite with ten tests that is cheap to change. Every command below was run as written, against provalab.io, with these versions:</p>
<table>
<thead>
<tr>
<th>Package</th>
<th>Version</th>
</tr>
</thead>
<tbody><tr>
<td>playwright</td>
<td>1.64.0</td>
</tr>
<tr>
<td>pytest-playwright</td>
<td>0.10.0</td>
</tr>
<tr>
<td>pytest</td>
<td>9.2.0</td>
</tr>
<tr>
<td>pytest-xdist</td>
<td>3.8.0</td>
</tr>
</tbody></table>
<h2>What we&#39;re building</h2>
<pre><code>playwright-in-practice/
├── pytest.ini
├── conftest.py          # fixtures: saved state, viewport, mocked API
├── pages/
│   ├── __init__.py
│   ├── blog.py          # BlogPage, PostPage
│   └── guide.py         # GuidePage
├── test_blog.py
├── test_consent.py
└── test_guides.py
</code></pre>
<p>Four ideas, one each for the four problems above:</p>
<ol>
<li><strong>Page objects</strong> so a locator lives in one place.</li>
<li><strong>Fixtures</strong> so a test asks for what it needs instead of building it.</li>
<li><strong>Saved browser state</strong> so the cookie banner is dismissed once, not ten times.</li>
<li><strong>A mocked API</strong> so the form test never creates a real lead.</li>
</ol>
<p>Then we run the whole thing on four workers.</p>
<p><img src="https://provalab.io/blog/handson/test-layers.svg" alt="Five layers. Tests say what should happen. Fixtures provide what every test needs. Page objects know where things are. Playwright drives the browser. The browser loads the site. When a button moves, only the page objects change." width="880" height="500" loading="lazy"></p>
<h2>Setup (2 minutes)</h2>
<p>Same folder and virtual environment routine as <a href="https://provalab.io/blog/first-playwright-test/">Part 2</a>, plus the parallel runner:</p>
<pre><code class="language-bash">mkdir playwright-in-practice &amp;&amp; cd playwright-in-practice
python3 -m venv .venv &amp;&amp; source .venv/bin/activate
pip install pytest-playwright pytest-xdist
playwright install chromium
</code></pre>
<p><code>pytest.ini</code> points at the site and silences one warning:</p>
<pre><code class="language-ini">[pytest]
addopts = --base-url https://provalab.io
filterwarnings = ignore::pytest.PytestRemovedIn10Warning
</code></pre>
<p>The second line is worth a sentence. pytest 9 deprecated a pattern that pytest-playwright 0.10 still uses for its <code>base_url</code> fixture, and prints a warning on every run. It is harmless, and the plugin will catch up; until then this keeps the output clean so that a warning you <em>should</em> read doesn&#39;t hide among ones you shouldn&#39;t.</p>
<h2>Step 1: Page objects (10 minutes)</h2>
<p>A page object is a small class that knows where things are on one page and what a user can do there. Tests use it instead of locators. Here is the blog index and a post:</p>
<pre><code class="language-python"># pages/blog.py
import re

from playwright.sync_api import Page


class PostPage:
    def __init__(self, page: Page):
        self.page = page
        self.title = page.get_by_role(&quot;heading&quot;, level=1)
        self.all_posts = page.get_by_role(&quot;link&quot;, name=&quot;All posts&quot;)

    URL = re.compile(r&quot;/blog/[a-z0-9-]+/$&quot;)


class BlogPage:
    def __init__(self, page: Page):
        self.page = page
        self.heading = page.get_by_role(&quot;heading&quot;, level=1)
        self.cards = page.get_by_test_id(&quot;blog-card&quot;)
        self.pdf_banner = page.get_by_test_id(&quot;blog-pdf-banner&quot;)

    def open(self):
        self.page.goto(&quot;/blog/&quot;)
        return self

    def open_first_post(self) -&gt; PostPage:
        self.cards.first.get_by_role(&quot;link&quot;).first.click()
        return PostPage(self.page)

    def pdf_link(self, series: str):
        return self.pdf_banner.get_by_role(&quot;link&quot;, name=f&quot;{series} PDF&quot;)
</code></pre>
<p>Two things to notice. The attributes are <strong>locators, not elements</strong>: <code>self.cards</code> is a description of how to find the cards, evaluated fresh every time a test uses it, so it never goes stale after navigation. And an action that leads somewhere <strong>returns the page object for where you end up</strong>: <code>open_first_post()</code> hands back a <code>PostPage</code>.</p>
<p>The second page object covers the free-PDF pages, which have a form:</p>
<pre><code class="language-python"># pages/guide.py
from playwright.sync_api import Page


class GuidePage:
    &quot;&quot;&quot;The free-PDF page for one series, e.g. /testing-basics/.&quot;&quot;&quot;

    def __init__(self, page: Page, slug: str):
        self.page = page
        self.slug = slug
        self.parts = page.get_by_test_id(&quot;pdf-part&quot;)
        self.download_link = page.get_by_test_id(&quot;download-link&quot;)
        self.error = page.get_by_test_id(&quot;lead-error&quot;)

    def open(self):
        self.page.goto(f&quot;/{self.slug}/&quot;)
        return self

    def request_pdf(self, name: str, email: str):
        self.page.get_by_test_id(&quot;lead-name&quot;).fill(name)
        self.page.get_by_test_id(&quot;lead-email&quot;).fill(email)
        self.page.get_by_test_id(&quot;lead-submit&quot;).click()
</code></pre>
<p><code>request_pdf</code> is the kind of method page objects exist for: three low-level steps that every test would otherwise repeat, named after what the user is doing.</p>
<p>Now the three tests from Part 2, rewritten. They read like sentences, and there is not a single selector in them:</p>
<pre><code class="language-python"># test_blog.py
from playwright.sync_api import expect

from pages.blog import PostPage


def test_blog_lists_posts(blog):
    expect(blog.heading).to_have_text(&quot;ProvaLab Blog&quot;)
    expect(blog.cards.first).to_be_visible()


def test_a_post_opens_from_the_list(blog):
    post = blog.open_first_post()
    expect(post.page).to_have_url(PostPage.URL)
    expect(post.title).to_be_visible()
    expect(post.all_posts).to_be_visible()


def test_pdf_banner_links_to_both_guides(blog):
    expect(blog.pdf_link(&quot;Testing Basics&quot;)).to_have_attribute(&quot;href&quot;, &quot;/testing-basics/&quot;)
    expect(blog.pdf_link(&quot;Hands-On Testing&quot;)).to_have_attribute(&quot;href&quot;, &quot;/hands-on-testing/&quot;)
</code></pre>
<p>Where does <code>blog</code> come from? That is the next step.</p>
<h2>Step 2: Fixtures (5 minutes)</h2>
<p>A pytest <strong>fixture</strong> is a function that builds something a test needs; a test asks for it by naming it as a parameter. You have been using them since Part 2: <code>page</code> is a fixture that pytest-playwright provides. Yours go in <code>conftest.py</code>, which pytest loads automatically:</p>
<pre><code class="language-python"># conftest.py (first part)
import pytest

from pages.blog import BlogPage


@pytest.fixture
def blog(page) -&gt; BlogPage:
    return BlogPage(page).open()
</code></pre>
<p>A fixture can ask for other fixtures. <code>blog</code> asks for <code>page</code>, wraps it and opens the blog. Every test that names <code>blog</code> gets a fresh page, already on <code>/blog/</code>, with no setup lines of its own.</p>
<p>pytest-playwright also lets you <strong>override</strong> its own fixtures. The most useful one is <code>browser_context_args</code>, the options every new browser context is created with. A fixed viewport and locale make screenshots and date formats the same on every machine:</p>
<pre><code class="language-python">@pytest.fixture(scope=&quot;session&quot;)
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        &quot;viewport&quot;: {&quot;width&quot;: 1280, &quot;height&quot;: 800},
        &quot;locale&quot;: &quot;en-GB&quot;,
    }
</code></pre>
<p><code>scope=&quot;session&quot;</code> means it runs once per test session, not once per test. Keep that word in mind for the next step.</p>
<h2>Step 3: Dismiss the cookie banner once (10 minutes)</h2>
<p>Every page on this site shows a small cookie notice until you close it. In Part 2 we ignored it, because none of the three tests clicked anything under it. In a bigger suite it will eventually sit on top of something you need, and the lazy fix, a click at the top of every test, is slow and gets copied everywhere.</p>
<p>Playwright has a better answer. A browser context can <strong>save its state</strong> (cookies and local storage) to a file, and a new context can <strong>start from that file</strong>. So: dismiss the banner once, save, and have every test&#39;s context begin with it already gone.</p>
<pre><code class="language-python"># conftest.py (second part)
import os
from pathlib import Path

# One state file per pytest-xdist worker, so parallel runs never write the same file.
STATE = Path(__file__).parent / &quot;.state&quot; / f&quot;consent-{os.environ.get(&#39;PYTEST_XDIST_WORKER&#39;, &#39;main&#39;)}.json&quot;


@pytest.fixture(scope=&quot;session&quot;)
def consent_state(browser, base_url) -&gt; Path:
    &quot;&quot;&quot;Dismiss the cookie banner once per session and save the browser state.&quot;&quot;&quot;
    if not STATE.exists():
        context = browser.new_context(base_url=base_url)
        page = context.new_page()
        page.goto(&quot;/&quot;)
        page.get_by_role(&quot;button&quot;, name=&quot;Dismiss&quot;).click()
        STATE.parent.mkdir(exist_ok=True)
        context.storage_state(path=STATE)
        context.close()
    return STATE


@pytest.fixture(scope=&quot;session&quot;)
def browser_context_args(browser_context_args, consent_state):
    &quot;&quot;&quot;Every test&#39;s browser context starts with the banner already dismissed.&quot;&quot;&quot;
    return {
        **browser_context_args,
        &quot;storage_state&quot;: str(consent_state),
        &quot;viewport&quot;: {&quot;width&quot;: 1280, &quot;height&quot;: 800},
        &quot;locale&quot;: &quot;en-GB&quot;,
    }
</code></pre>
<p><code>consent_state</code> is session-scoped, so the banner is dismissed once per run. <code>browser_context_args</code> now asks for it and passes the file as <code>storage_state</code>. Nothing else in the suite mentions the banner at all. The saved file is small and readable:</p>
<pre><code class="language-json">{&quot;cookies&quot;: [], &quot;origins&quot;: [{&quot;origin&quot;: &quot;https://provalab.io&quot;, &quot;localStorage&quot;: [{&quot;name&quot;: &quot;cookie-consent&quot;, &quot;value&quot;: &quot;dismissed&quot;}]}]}
</code></pre>
<h3>What went wrong the first time</h3>
<p>Our first version clicked the banner&#39;s <strong>OK</strong> button instead of <strong>Dismiss</strong>. The run was green, the state file was <code>{&quot;cookies&quot;: [], &quot;origins&quot;: []}</code>, and the banner was still there in every test. The home page has two buttons called &quot;OK&quot;, and the click had landed on the other one. The lesson is the one from <a href="https://provalab.io/blog/ui-testing-explained/">Part 1</a> again: a locator should describe one thing. &quot;Dismiss&quot; is the banner&#39;s only button with that accessible name, so that is the one the fixture clicks.</p>
<p>Two tests prove the state works, and show the difference between a context built from the fixture and a bare one:</p>
<pre><code class="language-python"># test_consent.py
from playwright.sync_api import expect

BANNER = &quot;We use cookies for analytics&quot;


def test_banner_is_gone_with_saved_state(page):
    page.goto(&quot;/&quot;)
    expect(page.get_by_text(BANNER)).to_have_count(0)


def test_banner_shows_for_a_fresh_visitor(browser, base_url):
    context = browser.new_context(base_url=base_url)   # no storage_state
    page = context.new_page()
    page.goto(&quot;/&quot;)
    expect(page.get_by_text(BANNER)).to_be_visible()
    context.close()
</code></pre>
<p>The same trick is how you handle <strong>login</strong>. Sign in once in a session fixture, save the state, and every test starts signed in. Put the state file in <code>.gitignore</code>: it holds whatever cookies the site set.</p>
<h2>Step 4: Mock the API so no real lead is created (10 minutes)</h2>
<p>The free-PDF pages have a form: name and email, then a download link appears. Behind it the page POSTs to a lead API. A test that fills the form for real creates a real lead every run, and someone has to delete &quot;Test Reader&quot; from the inbox each morning.</p>
<p><code>page.route</code> intercepts requests <strong>inside the browser</strong>, before they leave it. The test decides what the server says.</p>
<p><img src="https://provalab.io/blog/handson/mock-route.svg" alt="The page sends a POST to the leads API. page.route intercepts it inside the browser, records the body and replies with whatever the test says: 201 and the PDF link appears, or 422 and an error appears. The real API never receives the request." width="880" height="460" loading="lazy"></p>
<p>As a fixture, so any test can ask for it:</p>
<pre><code class="language-python"># conftest.py (third part)
@pytest.fixture
def mock_leads(page):
    &quot;&quot;&quot;Answer the lead API inside the browser, so tests never create a real lead.
    Returns the list of request bodies the page sent.&quot;&quot;&quot;
    sent = []

    def handle(route):
        sent.append(route.request.post_data_json)
        route.fulfill(status=201, json={&quot;id&quot;: 1})

    page.route(&quot;**/api/v1/public/leads&quot;, handle)
    return sent
</code></pre>
<p>The fixture returns the list it appends to, so a test can check <strong>what the page sent</strong>, not just what it showed afterwards. Here are the guide tests, parametrised over both PDF pages:</p>
<pre><code class="language-python"># test_guides.py
import pytest
from playwright.sync_api import expect

from pages.guide import GuidePage

GUIDES = [(&quot;testing-basics&quot;, 23), (&quot;hands-on-testing&quot;, 7)]


@pytest.mark.parametrize(&quot;slug,parts&quot;, GUIDES)
def test_guide_lists_every_part(page, slug, parts):
    guide = GuidePage(page, slug).open()
    expect(guide.parts).to_have_count(parts)


@pytest.mark.parametrize(&quot;slug,parts&quot;, GUIDES)
def test_pdf_appears_after_the_form(page, mock_leads, slug, parts):
    guide = GuidePage(page, slug).open()
    guide.request_pdf(&quot;Test Reader&quot;, &quot;reader@example.com&quot;)
    expect(guide.download_link).to_have_attribute(&quot;href&quot;, f&quot;/downloads/{slug}.pdf&quot;)
    assert len(mock_leads) == 1
    assert mock_leads[0][&quot;email&quot;] == &quot;reader@example.com&quot;
    assert mock_leads[0][&quot;source_path&quot;] == f&quot;/{slug}/&quot;


def test_rejected_submission_shows_an_error(page):
    page.route(&quot;**/api/v1/public/leads&quot;, lambda route: route.fulfill(status=422, json={&quot;detail&quot;: &quot;invalid&quot;}))
    guide = GuidePage(page, &quot;testing-basics&quot;).open()
    guide.request_pdf(&quot;Test Reader&quot;, &quot;reader@example.com&quot;)
    expect(guide.error).to_be_visible()
    expect(guide.download_link).to_have_count(0)
</code></pre>
<p><code>@pytest.mark.parametrize</code> runs a test once per row of <code>GUIDES</code>, so two tests become four, each with its own name in the output. The last test skips the fixture and routes inline, because it wants a different answer: a 422. That is the real value of mocking. A real server gives you the happy path all day; the sad paths, a rejection, a 500, a timeout, you get on demand only when you are the server.</p>
<p>One rule: mock the things <strong>around</strong> what you are testing, never the thing itself. Here the subject is the page&#39;s behaviour after an answer, so faking the answer is fair. A test of the lead API would have to call the lead API.</p>
<h2>Step 5: Run it, then run it in parallel (3 minutes)</h2>
<p>The whole suite, ten tests, one browser:</p>
<pre><code>$ pytest -v
test_blog.py::test_blog_lists_posts[chromium] PASSED                     [ 10%]
test_blog.py::test_a_post_opens_from_the_list[chromium] PASSED           [ 20%]
test_blog.py::test_pdf_banner_links_to_both_guides[chromium] PASSED      [ 30%]
test_consent.py::test_banner_is_gone_with_saved_state[chromium] PASSED   [ 40%]
test_consent.py::test_banner_shows_for_a_fresh_visitor[chromium] PASSED  [ 50%]
test_guides.py::test_guide_lists_every_part[chromium-testing-basics-23] PASSED [ 60%]
test_guides.py::test_guide_lists_every_part[chromium-hands-on-testing-7] PASSED [ 70%]
test_guides.py::test_pdf_appears_after_the_form[chromium-testing-basics-23] PASSED [ 80%]
test_guides.py::test_pdf_appears_after_the_form[chromium-hands-on-testing-7] PASSED [ 90%]
test_guides.py::test_rejected_submission_shows_an_error[chromium] PASSED [100%]
============================= 10 passed in 27.14s ==============================
</code></pre>
<p>Now the same suite on four workers. pytest-xdist starts four pytest processes and deals the tests out between them; each process gets its own browser and its own session fixtures, which is why the state file is named after the worker:</p>
<pre><code>$ pytest -q -n 4
..........                                                               [100%]
10 passed in 11.56s
</code></pre>
<table>
<thead>
<tr>
<th>Run</th>
<th>Time</th>
</tr>
</thead>
<tbody><tr>
<td><code>pytest</code> (one worker)</td>
<td>27.1 s</td>
</tr>
<tr>
<td><code>pytest -n 4</code></td>
<td>11.6 s</td>
</tr>
</tbody></table>
<p>Ten tests don&#39;t need four workers. A hundred do, and parallel only works if the tests were written as if they would run in any order, each starting from its own <code>goto()</code> and sharing nothing but read-only state. That is why Part 2&#39;s habit, &quot;no test depends on another&quot;, matters before the suite is big, not after.</p>
<p>Everything from Part 2 still applies on top: <code>--headed</code> to watch, <code>--tracing retain-on-failure</code> to keep a trace of anything red, <code>--browser firefox</code> to run the same ten tests in a second engine.</p>
<h2>Habits worth keeping</h2>
<ol>
<li><strong>Page objects hold locators and actions, never assertions.</strong> The test decides what should be true; the page object only knows where to look.</li>
<li><strong>A fixture per recurring need.</strong> If two tests share three setup lines, that is a fixture.</li>
<li><strong>Session scope for anything expensive and read-only</strong>: saved state, a signed-in session, test data that doesn&#39;t change.</li>
<li><strong>Mock what creates real records, and the sad paths.</strong> Everything else, hit for real.</li>
<li><strong>Write every test to run alone and in any order.</strong> Then <code>-n 4</code> is free when you need it.</li>
</ol>
<h2>What&#39;s next</h2>
<p>This suite runs on one laptop and one browser. The next step is to run it on the browsers and devices your users actually have, keep every trace somewhere the whole team can see it, and have failures land with the agent that wrote the code. That is what ProvaLab&#39;s QA Infrastructure does: the same Playwright tests connect to real browsers and devices in the cloud, every session is recorded, and a failing test&#39;s trace goes straight back to your coding agent through the ProvaLab plugin or MCP. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
<p>If you came here without doing Part 2 first, start at <a href="https://provalab.io/blog/first-playwright-test/">Your first Playwright test</a>. The whole Hands-On series, seven parts, is also <a href="https://provalab.io/hands-on-testing/">one free PDF</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>50 software testing interview questions, with short answers and where to read more</title>
      <link>https://provalab.io/blog/qa-interview-questions/</link>
      <guid isPermaLink="true">https://provalab.io/blog/qa-interview-questions/</guid>
      <pubDate>Fri, 30 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>qa-interview</category>
      <category>software-testing</category>
      <category>careers</category>
      <description>The questions QA and test automation interviews actually ask, grouped by topic, each with a two-sentence answer and a link to the full explanation in our Testing Basics and Hands-On series.</description>
      <content:encoded><![CDATA[<p>Every question below is one we&#39;d expect in an interview for a QA, test or automation role, from junior to mid-level. Each gets a short answer you could say out loud, and a link to the post that explains it properly. Read the short answers to revise; follow the links for anything you couldn&#39;t explain to a colleague.</p>
<p><img src="https://provalab.io/blog/handson/interview-topic-map.svg" alt="Ten topic cards: fundamentals (7 questions, Testing Basics 1, 2, 19), process and planning (6, parts 4, 8, 23), test design (6, parts 6, 7), bugs and reporting (5, parts 9, 10), test types (7, parts 12 to 16), runs, coverage and traceability (5, parts 11, 17, 18), automation and tools (5, Basics 3 and 5, Hands-On 1 and 2), API and security (5, Hands-On 3 to 7), agile, CI/CD and behavioural (4, part 23)." width="880" height="470" loading="lazy"></p>
<h2>Fundamentals</h2>
<p><strong>1. What is software testing?</strong>
Checking that software does what it should, and doesn&#39;t do what it shouldn&#39;t. A test has a starting point, an action and an expected result; a mismatch is a bug. (<a href="https://provalab.io/blog/what-is-software-testing/">Part 1</a>)</p>
<p><strong>2. Can testing prove software is bug-free?</strong>
No. Testing shows the presence of defects, never their absence. Its job is to find the important bugs before users do, and to report the risk that remains. (<a href="https://provalab.io/blog/seven-principles-of-testing/">The 7 principles</a>)</p>
<p><strong>3. Why is exhaustive testing impossible?</strong>
The number of possible inputs and paths is astronomically large; a ten-character field alone has quadrillions of values. So we choose tests by risk and by technique. (<a href="https://provalab.io/blog/seven-principles-of-testing/">The 7 principles</a>)</p>
<p><strong>4. What are the levels of testing?</strong>
Unit (one piece of code), integration (pieces together), system (the whole application, end to end) and acceptance (does it meet the real need). Each catches what the others miss. (<a href="https://provalab.io/blog/types-of-software-testing/">Part 2</a>)</p>
<p><strong>5. Explain the testing pyramid.</strong>
Many fast unit tests at the base, fewer integration tests, and a few slow end-to-end tests at the top. The inverted version, mostly end-to-end tests, is slow and flaky. (<a href="https://provalab.io/blog/types-of-software-testing/">Part 2</a>)</p>
<p><strong>6. What is the difference between QA, QC and testing?</strong>
QA improves the process to prevent defects; QC checks the product to find them; testing is the main QC activity. Job titles blur this, but the definitions don&#39;t. (<a href="https://provalab.io/blog/qa-vs-qc-verification-vs-validation/">Part 20</a>)</p>
<p><strong>7. Verification vs validation?</strong>
Verification: are we building the product right, against its specification? Validation: are we building the right product, for its users? A product can pass one and fail the other. (<a href="https://provalab.io/blog/qa-vs-qc-verification-vs-validation/">Part 20</a>)</p>
<h2>Process and planning</h2>
<p><strong>8. What is the STLC?</strong>
The software testing life cycle: requirement analysis, test planning, test case design, environment setup, execution and closure, each with entry and exit criteria. It runs alongside development, not after it. (<a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">Part 4</a>)</p>
<p><strong>9. What are entry and exit criteria?</strong>
Entry: what must be true before a phase starts, such as &quot;build deployed, smoke test passed&quot;. Exit: what must be true to finish, such as &quot;all high-priority tests passed, no open critical bugs&quot;. They stop testing starting too early or never ending. (<a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">Part 4</a>)</p>
<p><strong>10. What goes in a test plan?</strong>
Objective, scope in and out, approach, environment, entry and exit criteria, people, schedule, deliverables and risks. The most underrated line is &quot;out of scope&quot;. (<a href="https://provalab.io/blog/how-to-write-a-test-plan/">Part 8</a>)</p>
<p><strong>11. Test plan vs test strategy?</strong>
A strategy is the organisation&#39;s long-term approach; a plan applies it to one release or project. (<a href="https://provalab.io/blog/how-to-write-a-test-plan/">Part 8</a>)</p>
<p><strong>12. What is risk-based testing?</strong>
Spending the most testing effort where failure is both likely and harmful. Score each area on likelihood and impact, and test the top-right of that grid first and deepest. (<a href="https://provalab.io/blog/how-to-write-a-test-plan/">Part 8</a>)</p>
<p><strong>13. What does &quot;shift left&quot; mean?</strong>
Moving testing earlier: reviewing requirements, writing tests before code, running checks on every commit. Bugs found earlier are cheaper to fix. (<a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Part 23</a>)</p>
<h2>Test design</h2>
<p><strong>14. What is a test case, and what does a good one contain?</strong>
A set of steps with specific data that checks one thing and states the expected result. It has an ID, a title, preconditions, test data, steps, an expected result and a link to its requirement. (<a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>)</p>
<p><strong>15. Test scenario vs test case?</strong>
A scenario is what to test, in one line; a test case is exactly how to test one part of it. One scenario becomes several cases. (<a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>)</p>
<p><strong>16. What is boundary value analysis?</strong>
Testing at and around the edges of a valid range, because bugs like <code>&lt;</code> instead of <code>&lt;=</code> only show at the boundary. For a 0 to 100 field: -1, 0, 1, 99, 100, 101. (<a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>)</p>
<p><strong>17. What is equivalence partitioning?</strong>
Grouping inputs the software should treat the same, and testing one value from each group instead of all of them. (<a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>)</p>
<p><strong>18. Black-box vs white-box vs grey-box testing?</strong>
Black-box tests from the outside, against requirements. White-box designs tests from the code&#39;s structure. Grey-box tests from outside with some knowledge of the inside, such as the database or the API. (<a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Part 7</a>)</p>
<p><strong>19. Statement coverage vs branch coverage?</strong>
Statement: did every line run? Branch: did every decision go both true and false? One test can give 100% statement and only 50% branch coverage. (<a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Part 7</a>)</p>
<h2>Bugs and reporting</h2>
<p><strong>20. What makes a good bug report?</strong>
A title that says where, what and when; the environment; the fewest steps to reproduce; expected vs actual result; severity and priority; and evidence. Test: could a stranger reproduce it without asking you anything? (<a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a>)</p>
<p><strong>21. Severity vs priority?</strong>
Severity is how badly the bug breaks the software; priority is how soon it must be fixed. A misspelled brand name on the home page is low severity, high priority. (<a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a>)</p>
<p><strong>22. Describe the bug life cycle.</strong>
New, assigned, open, fixed, ready to retest, retest, verified, closed; or reopened if the retest fails. Side exits: rejected, duplicate, deferred, can&#39;t reproduce. (<a href="https://provalab.io/blog/bug-life-cycle/">Part 10</a>)</p>
<p><strong>23. Who should verify a fix?</strong>
Ideally the person who found the bug. They know exactly what was wrong, and they retest on every platform the bug affected. (<a href="https://provalab.io/blog/bug-life-cycle/">Part 10</a>)</p>
<p><strong>24. What do you do when a developer says &quot;works on my machine&quot;?</strong>
Add what the report was missing: exact data, environment, a recording. &quot;Can&#39;t reproduce&quot; usually means the report lacked something, not that the bug isn&#39;t real. (<a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a>)</p>
<h2>Test types</h2>
<p><strong>25. Smoke vs sanity testing?</strong>
Smoke: a wide, shallow check of every main feature on each new build, to decide whether it&#39;s worth testing at all. Sanity: a narrow, deeper check of one area after a change. (<a href="https://provalab.io/blog/smoke-vs-sanity-testing/">Part 14</a>)</p>
<p><strong>26. Regression testing vs re-testing?</strong>
Re-testing confirms a specific bug is fixed by running the failed test again. Regression testing runs previously passing tests to check the change broke nothing else. Every fix needs both. (<a href="https://provalab.io/blog/regression-testing-vs-retesting/">Part 15</a>)</p>
<p><strong>27. What is integration testing, and what are stubs and drivers?</strong>
Testing that separately built parts work together. A stub stands in for a part that is called; a driver stands in for a part that calls. Top-down integration uses stubs, bottom-up uses drivers. (<a href="https://provalab.io/blog/integration-testing/">Part 12</a>)</p>
<p><strong>28. System testing vs end-to-end testing?</strong>
End-to-end follows one complete user journey; system testing covers those journeys plus non-functional checks of the whole system, in a production-like environment. (<a href="https://provalab.io/blog/system-testing/">Part 13</a>)</p>
<p><strong>29. Name the types of performance testing.</strong>
Load (expected peak), stress (past the peak, to the breaking point), spike (sudden rush), soak (hours, for leaks), volume (large data), scalability (does adding servers add capacity). (<a href="https://provalab.io/blog/performance-testing-types/">Part 16</a>)</p>
<p><strong>30. Why use percentiles instead of the average response time?</strong>
The average hides the slow tail. A p95 of 1.8 s means one in twenty users waits that long, even if the average looks fine. (<a href="https://provalab.io/blog/performance-testing-types/">Part 16</a>)</p>
<p><strong>31. What is static testing?</strong>
Finding defects without running the software: reviews, walkthroughs, inspections and static analysis tools. It catches missing requirements and design flaws that no executed test can. (<a href="https://provalab.io/blog/static-testing/">Part 21</a>)</p>
<h2>Runs, coverage and traceability</h2>
<p><strong>32. Test case vs test run vs test result?</strong>
A test case is written once; a run executes a set of cases on a specific build and environment; a result is what happened to one case in one run. (<a href="https://provalab.io/blog/test-runs-and-test-results/">Part 17</a>)</p>
<p><strong>33. What do blocked and skipped mean, and why not count them as failed?</strong>
Blocked: the test couldn&#39;t run because something else is broken. Skipped: deliberately not run. Neither tells you anything about the feature, so they&#39;re reported separately. (<a href="https://provalab.io/blog/test-runs-and-test-results/">Part 17</a>)</p>
<p><strong>34. How do you calculate pass rate?</strong>
Of executed tests: passed ÷ (passed + failed). Of planned tests: passed ÷ all planned. Report both; the gap is the blocked and skipped tests. (<a href="https://provalab.io/blog/test-runs-and-test-results/">Part 17</a>)</p>
<p><strong>35. What is a requirements traceability matrix?</strong>
A table linking each requirement to its test cases, results and bugs. It shows requirements with no tests, failing requirements, and tests that cover nothing. (<a href="https://provalab.io/blog/requirements-traceability-matrix/">Part 11</a>)</p>
<p><strong>36. What kinds of test coverage are there?</strong>
Requirement coverage, execution coverage, code coverage (line, branch, function) and platform coverage. Each has a formula and none of them means bug-free at 100%. (<a href="https://provalab.io/blog/test-coverage-explained/">Part 18</a>)</p>
<h2>Automation and tools</h2>
<p><strong>37. How do you decide what to automate?</strong>
Three questions: will it run again and again, is the feature stable, is pass/fail clear? Yes to all three, automate; any no, keep it manual for now. (<a href="https://provalab.io/blog/manual-vs-automated-testing/">Part 3</a>)</p>
<p><strong>38. What is a flaky test, and why is it dangerous?</strong>
One that passes and fails on different runs with no code change. Once people re-run red tests until they pass, real failures get re-run away too. (<a href="https://provalab.io/blog/ui-testing-explained/">Hands-On 1</a>)</p>
<p><strong>39. What&#39;s the best way to locate elements in a UI test?</strong>
By role and accessible name first, then test IDs, then visible text; CSS tied to styling and positional XPath last. The locator decides whether the test survives a redesign. (<a href="https://provalab.io/blog/ui-testing-explained/">Hands-On 1</a>)</p>
<p><strong>40. Selenium vs Playwright vs Cypress?</strong>
Selenium: the standard, most languages, widest browser and device coverage, manual waiting. Playwright: fast, auto-waiting, all three browser engines, best debugging. Cypress: best developer experience for JavaScript front-end teams. New projects usually pick Playwright. (<a href="https://provalab.io/blog/ui-testing-explained/">Hands-On 1</a>)</p>
<p><strong>41. What is TDD?</strong>
Write a failing test first, write just enough code to pass it, then clean up. The failing test proves the test can catch the bug. (<a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Part 23</a>)</p>
<h2>API and security</h2>
<p><strong>42. Why test the API before the UI?</strong>
API tests run in milliseconds, are stable, start before the screens exist, pinpoint the failing endpoint and cover every client at once. Test the business rules there; keep a few UI tests for journeys. (<a href="https://provalab.io/blog/api-testing-explained/">Hands-On 3</a>)</p>
<p><strong>43. 401 vs 403 vs 404?</strong>
401: not authenticated, who are you? 403: authenticated but not allowed. 404: not found. Assert the exact one; an API that returns 404 for someone else&#39;s record leaks that it exists. (<a href="https://provalab.io/blog/first-api-test-pytest/">Hands-On 4</a>)</p>
<p><strong>44. What is the most common serious API security bug?</strong>
Broken access control: a user reading or changing another user&#39;s data. The test is four lines: sign in as one user, request another&#39;s record, expect 403. (<a href="https://provalab.io/blog/security-testing-basics-owasp-top-10/">Hands-On 5</a>)</p>
<p><strong>45. What is the OWASP Top 10?</strong>
The ten most serious web application risks, updated from real-world data. The 2025 edition leads with broken access control and misconfiguration, and adds supply chain failures. (<a href="https://provalab.io/blog/security-testing-basics-owasp-top-10/">Hands-On 5</a>)</p>
<p><strong>46. SAST vs DAST?</strong>
SAST reads the source code without running it, on every commit. DAST attacks the running application from outside, nightly or before release. They find different things. (<a href="https://provalab.io/blog/first-security-scan-owasp-zap/">Hands-On 6</a>)</p>
<h2>Agile, CI/CD and you</h2>
<p><strong>47. How does testing work in an agile team?</strong>
Quality is the whole team&#39;s job, testing happens inside the sprint, acceptance criteria are the test cases, and regression is automated so people can explore. (<a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Part 23</a>)</p>
<p><strong>48. What runs at each stage of a CI/CD pipeline?</strong>
Every commit: static analysis and unit tests. Every pull request: integration and API tests. Merge: build and smoke test. Nightly: full regression and performance. Release candidate: system and exploratory testing. Production: smoke test and monitoring. (<a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Part 23</a>)</p>
<p><strong>49. Tell me about a bug you found that the tests missed.</strong>
Pick one that came from exploring beyond the script: an unusual sequence, an edge case, a state nobody considered. Explain how you found it, how you reported it, and what test you added so it stays fixed. (<a href="https://provalab.io/blog/exploratory-testing/">Part 22</a>)</p>
<p><strong>50. How do you decide testing is done?</strong>
Against the exit criteria agreed in the plan, never against the deadline alone. Report what was tested, what wasn&#39;t, what&#39;s still open and the risk that remains. (<a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">Part 4</a>)</p>
<h2>How to use this before an interview</h2>
<ol>
<li>Read the short answers once. Mark any you couldn&#39;t explain to a colleague.</li>
<li>Follow the link for each marked one; every post is a ten-minute read.</li>
<li>For questions 7, 21, 25, 26 and 43, the pairs people confuse, practise saying the difference out loud.</li>
<li>Prepare one real story for question 49. Interviewers remember stories, not definitions.</li>
</ol>
<p>Good luck. The whole <a href="https://provalab.io/blog/what-is-software-testing/">Testing Basics</a> series and the <a href="https://provalab.io/blog/ui-testing-explained/">Hands-On Testing</a> series are free, and every code sample in them actually runs.</p>
]]></content:encoded>
    </item>
    <item>
      <title>API testing explained: what it is, why test the API before the UI, and what to check</title>
      <link>https://provalab.io/blog/api-testing-explained/</link>
      <guid isPermaLink="true">https://provalab.io/blog/api-testing-explained/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>api-testing</category>
      <category>rest</category>
      <description>What an API is and how API tests work, why they are faster and more stable than UI tests, the five things to check in every response, the kinds of API tests, and the tools teams use.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/first-playwright-test/">Part 2</a> our three browser tests took about 11 seconds to run. Most of that time was spent loading pages, rendering them and waiting for a browser, not checking anything. There is a faster place to test most of what matters, one layer down from the screens: <strong>the API</strong>.</p>
<h2>What is an API?</h2>
<p>An <strong>API</strong> (application programming interface) is the set of requests a program can make to another program. In a modern app, every screen talks to the server through an API: when you tap &quot;Place order&quot;, the mobile app sends a request like <code>POST /orders</code> with the order details, and the server replies with the result.</p>
<p>Most web and mobile APIs are <strong>HTTP APIs</strong>, usually following the <strong>REST</strong> style:</p>
<ul>
<li>A request names a <strong>resource</strong> with a URL path, such as <code>/orders/42</code>.</li>
<li>A <strong>method</strong> says what to do with it: <code>GET</code> (read), <code>POST</code> (create), <code>PUT</code> or <code>PATCH</code> (update), <code>DELETE</code>.</li>
<li>Data travels as <strong>JSON</strong>, a plain-text format of fields and values.</li>
<li>The reply carries a <strong>status code</strong>: <code>200</code> means OK, <code>201</code> created, <code>400</code> bad request, <code>401</code> not signed in, <code>403</code> not allowed, <code>404</code> not found, <code>500</code> the server broke.</li>
</ul>
<p><img src="https://provalab.io/blog/handson/api-layer.svg" alt="Three layers. Top: the mobile app, the website and the restaurant dashboard. Middle: the API, endpoints such as POST /orders, GET /orders/42 and POST /discounts/apply, which every interface calls. Bottom: the services and database. A UI test drives a screen through all three layers in seconds; an API test sends a request straight to the middle layer in milliseconds, with no browser." width="880" height="440" loading="lazy"></p>
<p><strong>The API is where the real logic lives.</strong> Whether a customer orders from the mobile app, the website or the restaurant dashboard, the same <code>POST /discounts/apply</code> decides what the discount is. A bug there shows up once, for everyone.</p>
<h2>What is API testing?</h2>
<p><strong>API testing sends requests directly to the API and checks the responses, without any user interface.</strong></p>
<p>A test is still <a href="https://provalab.io/blog/ui-testing-explained/">locate, act, assert</a>, with &quot;locate&quot; replaced by &quot;build the request&quot;:</p>
<ol>
<li><strong>Build</strong> a request: method, URL, headers, body.</li>
<li><strong>Send</strong> it.</li>
<li><strong>Assert</strong> on the response: status, headers, body and timing.</li>
</ol>
<p>In Python with the <code>requests</code> library it looks like this (we&#39;ll run tests like it for real in Part 4):</p>
<pre><code class="language-python">response = requests.post(
    &quot;https://api.example.com/discounts/apply&quot;,
    json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;},
    headers={&quot;Authorization&quot;: f&quot;Bearer {token}&quot;},
)
assert response.status_code == 200
assert response.json()[&quot;total&quot;] == 450
</code></pre>
<p>No browser, no page load, no waiting for elements. That test runs in a few milliseconds.</p>
<h2>Why test the API before the UI</h2>
<table>
<thead>
<tr>
<th></th>
<th>API tests</th>
<th>UI tests</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Speed</strong></td>
<td>Milliseconds each; thousands in a minute</td>
<td>Seconds each</td>
</tr>
<tr>
<td><strong>Stability</strong></td>
<td>Very stable: no timing, no layout, no cookie banners</td>
<td>The flakiest kind of test</td>
</tr>
<tr>
<td><strong>When you can start</strong></td>
<td>As soon as the endpoint exists, often before the screen does</td>
<td>Only once the screen is built</td>
</tr>
<tr>
<td><strong>Finds</strong></td>
<td>Logic, data, validation, security and error-handling bugs</td>
<td>What the user actually sees</td>
</tr>
<tr>
<td><strong>Pinpoints the cause</strong></td>
<td>Yes: one endpoint, one response</td>
<td>Rarely: the failure could be anywhere in the stack</td>
</tr>
<tr>
<td><strong>Covers</strong></td>
<td>Every client at once (app, website, partners)</td>
<td>One screen on one client</td>
</tr>
</tbody></table>
<p>The pattern most teams settle on is the <a href="https://provalab.io/blog/types-of-software-testing/">testing pyramid</a> applied to the back end: <strong>test the business rules thoroughly at the API level, and keep a few UI tests for the journeys users see.</strong> The discount logic gets fifty API tests and one UI test, not the other way round.</p>
<p>A second reason that&#39;s easy to miss: <strong>APIs are products too.</strong> If partners or a mobile team build on your API, a change that breaks it breaks them, whether or not your own website still works.</p>
<h2>What to check in every response</h2>
<p><img src="https://provalab.io/blog/handson/api-response-checks.svg" alt="A request, POST /discounts/apply with order_total 500 and code SAVE10, and its response. Five checks: 1, the status code, 200 here; 2, the headers, right content type and nothing secret leaked; 3, the body shape, the fields and types the contract promises; 4, the values, total 450 and discount 50, the business rule; 5, the timing, 42 ms, within budget." width="880" height="500" loading="lazy"></p>
<ol>
<li><strong>Status code.</strong> <code>200</code> for success, the <em>right</em> <code>4xx</code> for bad input or missing permission, and never a <code>500</code>. A <code>500</code> means the server crashed instead of handling the situation.</li>
<li><strong>Headers.</strong> The content type is what the client expects (<code>application/json</code>); caching is right for the data; and nothing sensitive leaks in a header.</li>
<li><strong>Body shape, the contract.</strong> The fields the API promises are present, with the right types: <code>total</code> is a number, not the string <code>&quot;450&quot;</code>. Clients break on shape changes even when the values are right.</li>
<li><strong>Values, the business rule.</strong> 500 with <code>SAVE10</code> must give <code>total: 450</code>. This is the check that finds the bug that charges every customer the wrong amount.</li>
<li><strong>Timing.</strong> The response arrived within the budget, say under 500 ms. One slow endpoint makes every screen that uses it slow.</li>
</ol>
<h2>Kinds of API tests</h2>
<p><strong>Functional tests</strong> check that each endpoint does its job: create an order, read it back, apply a discount. The bulk of your API suite.</p>
<p><strong>Negative tests</strong> send wrong input on purpose, and check that the API refuses it <em>cleanly</em>: a missing field returns <code>400</code> with a helpful message, an unknown order returns <code>404</code>, a discount of 150% is rejected. Many real bugs hide here, as we saw in <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">test design</a>.</p>
<p><strong>Authentication and authorisation tests</strong> check that requests without a valid token get <code>401</code>, and that a signed-in customer gets <code>403</code> when asking for <em>someone else&#39;s</em> order. The second kind, called broken object-level authorisation, is the most common serious API security bug, and it&#39;s easy to test: sign in as customer A, request customer B&#39;s order, expect <code>403</code>.</p>
<p><strong>Contract tests</strong> check the shape of requests and responses against an agreed definition, usually an <strong>OpenAPI</strong> (formerly Swagger) document. When several teams or partners depend on the API, contract tests catch a renamed field before it breaks anyone. Tools such as Pact go a step further, letting each client declare exactly what it needs.</p>
<p><strong>Integration tests</strong> through the API check that a sequence works end to end: create an order, apply a code, pay, then read the order back and confirm the total. This is <a href="https://provalab.io/blog/integration-testing/">integration testing</a> with the API as the entry point.</p>
<p><strong>Performance tests</strong> send many requests at once to find the breaking point, as described in <a href="https://provalab.io/blog/performance-testing-types/">performance testing</a>. Load tests are nearly always API tests.</p>
<h2>Test data and authentication</h2>
<p>Two practical problems every API tester meets:</p>
<ul>
<li><strong>Tokens.</strong> Most APIs need a key or a bearer token. Tests get one in a setup step (sign in with a test account, or read a test key from an environment variable), then reuse it. <strong>Never put real credentials in the test code or the repository.</strong></li>
<li><strong>Test data.</strong> <code>POST /orders</code> creates real rows. Tests should create what they need, use it, and clean up, or run against a test environment that&#39;s reset regularly. A test that depends on &quot;order 42 already existing&quot; will fail the day someone deletes order 42.</li>
</ul>
<h2>Tools</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Style</th>
<th>Good for</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Postman</strong> / <strong>Bruno</strong></td>
<td>Point-and-click, with collections you can run from the command line</td>
<td>Exploring an API, sharing requests with a team</td>
</tr>
<tr>
<td><strong>pytest + requests</strong> or <strong>httpx</strong></td>
<td>Python code</td>
<td>Suites that live with the code and run in CI; what we use in Part 4</td>
</tr>
<tr>
<td><strong>REST Assured</strong></td>
<td>Java</td>
<td>Java teams</td>
</tr>
<tr>
<td><strong>Karate</strong></td>
<td>Its own readable script language</td>
<td>Teams that want tests non-developers can read</td>
</tr>
<tr>
<td><strong>Playwright&#39;s request API</strong></td>
<td>JavaScript, Python</td>
<td>Mixing API calls into UI tests, such as creating data before a browser test</td>
</tr>
<tr>
<td><strong>Schemathesis</strong> / <strong>Dredd</strong></td>
<td>Generate tests from an OpenAPI document</td>
<td>Contract checking and fuzzing</td>
</tr>
</tbody></table>
<p>If you&#39;re starting from nothing, <strong>pytest with requests</strong> needs the least setup and produces tests that live next to your code.</p>
<h2>API testing and the UI tests you already have</h2>
<p>The two work together. The fastest reliable UI suites use the API to <strong>set the stage</strong>: create the test customer and the order through API calls in a few milliseconds, then open the browser only for the part that needs a browser. The sign-up form gets one UI test; the thousand ways an order can be wrong get API tests.</p>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/first-api-test-pytest/">Part 4</a></strong> we build a small API of our own, so there&#39;s nothing to sign up for and nothing we don&#39;t own, and write a pytest suite against it: functional, negative, authorisation and contract checks, with the real output.</p>
<p>ProvaLab&#39;s QA AI Agent runs API test cases on its own and records each request and response against its test case, and AppSec fuzzes your API for the authorisation and input-handling bugs described above. See it in the <a href="https://provalab.io/#stage-agent">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Test design techniques: black-box, white-box and grey-box testing</title>
      <link>https://provalab.io/blog/black-box-white-box-grey-box-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/black-box-white-box-grey-box-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-design</category>
      <description>The three ways of looking at software when you design tests, and the techniques for each, from boundary values and state transitions to statement and branch coverage.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a> we learned how to <em>write</em> a test case. This part is about how to <em>find</em> the right ones. Testers group their techniques by one simple question: <strong>how much of the inside of the software can you see?</strong></p>
<p>That gives three approaches: <strong>black-box</strong>, <strong>white-box</strong> and <strong>grey-box</strong> testing.</p>
<p><img src="https://provalab.io/blog/basics/black-white-grey-box.svg" alt="Black-box testing sees only inputs and outputs and tests like a user, using partitions, boundaries, decision tables and states. Grey-box testing sees some internals such as the database, APIs and logs. White-box testing sees the code and tests every line and branch, using statement, branch, condition and path coverage." width="880" height="430" loading="lazy"></p>
<h2>Black-box testing</h2>
<p><strong>In black-box testing you test without looking at the code.</strong> You know what goes in, what should come out, and what the requirements say. The inside is a closed box.</p>
<p>This is how users experience software, and it is how most QA testing is done. You need no programming knowledge, and the tests stay valid even when developers rewrite the code inside.</p>
<p>The main black-box techniques:</p>
<h3>1. Equivalence partitioning</h3>
<p>Split the inputs into groups the software should treat the same, and test <strong>one value from each group</strong>. A discount field that accepts 0–100% has three groups: below 0, 0 to 100, above 100. (More detail, with a diagram, in <a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>.)</p>
<h3>2. Boundary value analysis</h3>
<p>Test <strong>at and around every edge</strong>: -1, 0, 1, 99, 100 and 101 for that discount field. Mistakes like <code>&lt;</code> instead of <code>&lt;=</code> only show up at the exact boundary.</p>
<h3>3. Decision table testing</h3>
<p>When the result depends on <strong>combinations of conditions</strong>, list every combination in a table, and turn each row into a test. For example: new customer yes/no × order over 300 yes/no → free delivery yes/no.</p>
<h3>4. State transition testing</h3>
<p>Some features behave differently depending on <strong>what state they are in</strong>. A food order moves through clear states:</p>
<p><img src="https://provalab.io/blog/basics/order-states.svg" alt="An order moves Placed, Accepted, Preparing, Out for delivery, Delivered. It can be Cancelled from Placed, Accepted or Preparing. Cancelling once it is out for delivery is not allowed and should be tested." width="880" height="360" loading="lazy"></p>
<p>Draw the states and the allowed moves, then test:</p>
<ul>
<li><strong>Every allowed move:</strong> placed → accepted, preparing → cancelled, and so on.</li>
<li><strong>The forbidden moves:</strong> can a customer cancel an order that is already out for delivery? They mustn&#39;t, and the app must say so clearly.</li>
</ul>
<p>State transition testing is ideal for orders, payments, sign-in (locked after 3 wrong passwords?), subscriptions and any kind of workflow.</p>
<h3>5. Use case testing</h3>
<p>Test complete journeys the way a real user would: &quot;A customer searches for pizza, adds two, applies a code, pays by card and tracks the delivery.&quot; Each use case has a main flow and alternative flows (the card is declined, the restaurant closes mid-order).</p>
<h3>6. Pairwise testing</h3>
<p>When many settings combine, testing every combination explodes. Three devices × three payment methods × two languages makes 18 combinations. Most bugs are triggered by <strong>one setting, or a pair of settings</strong>, so <strong>pairwise testing</strong> picks a smaller set where every <em>pair</em> appears at least once. Here, 9 tests cover every pair instead of all 18. Free tools such as PICT generate these sets for you.</p>
<h2>White-box testing</h2>
<p><strong>In white-box testing you design tests by looking at the code itself.</strong> You read the logic and make sure every part of it is exercised. It is mostly done by developers, usually as unit tests (see <a href="https://provalab.io/blog/first-automated-test-pytest/">Part 5</a>).</p>
<p>The key white-box idea is <strong>code coverage</strong>: how much of the code your tests actually ran. Here is a small function from our delivery app:</p>
<pre><code class="language-python">def delivery_fee(order_total, is_member):
    fee = 40
    if order_total &gt;= 300:
        fee = 0
    if is_member:
        fee = fee / 2
    return fee
</code></pre>
<h3>Statement coverage</h3>
<p><strong>Has every line run at least once?</strong> One test is enough here:</p>
<pre><code class="language-python">def test_big_order_member():
    assert delivery_fee(400, is_member=True) == 0
</code></pre>
<p>Both <code>if</code> conditions are true, so every line runs. We measured it with pytest-cov:</p>
<pre><code class="language-text">Name          Stmts   Miss  Cover
delivery.py       7      0   100%
</code></pre>
<p>100% statement coverage, from one test. Sounds complete, but it isn&#39;t.</p>
<h3>Branch coverage</h3>
<p><strong>Has every decision gone both ways?</strong> Each <code>if</code> can be true <em>or</em> false, and our single test only ever took the &quot;true&quot; side. Turn on branch coverage and the gap appears:</p>
<pre><code class="language-text">Name          Stmts   Miss Branch BrPart  Cover   Missing
delivery.py       7      0      4      2    82%   3-&gt;5, 5-&gt;7
</code></pre>
<p><code>3-&gt;5</code> means &quot;the <code>if</code> on line 3 never skipped straight to line 5&quot;: we never tested a small order. Add one test where both conditions are false:</p>
<pre><code class="language-python">def test_small_order_non_member():
    assert delivery_fee(100, is_member=False) == 40
</code></pre>
<pre><code class="language-text">Name          Stmts   Miss Branch BrPart  Cover   Missing
delivery.py       7      0      4      0   100%
</code></pre>
<p>Now every decision has gone both ways. This is why <strong>branch coverage is a stronger measure than statement coverage</strong>.</p>
<h3>Condition and path coverage</h3>
<ul>
<li><strong>Condition coverage</strong> checks each part of a combined condition separately, such as <code>if is_member and order_total &gt;= 300</code>: has each half been both true and false?</li>
<li><strong>Path coverage</strong> checks every route through the code. Our function has two <code>if</code>s, so four paths: big+member, big+non-member, small+member, small+non-member. Our two tests cover only two of them. The small-order member case (fee 20) has never been tested.</li>
</ul>
<p>Path coverage grows very fast with real code, so teams usually aim for good branch coverage and add path tests only for critical logic.</p>
<p><strong>One warning:</strong> coverage tells you what code <em>ran</em>, not whether the tests <em>checked</em> the right things. A test with no <code>assert</code> still counts as coverage. Treat coverage as a map of what you haven&#39;t tested, not as proof of quality.</p>
<h2>Grey-box testing</h2>
<p><strong>Grey-box testing sits in between.</strong> You test from the outside, like a user, but you use some knowledge of the inside (the database design, the APIs, the logs or the architecture) to design sharper tests and check results more deeply.</p>
<p>Examples from our app:</p>
<ul>
<li><strong>Check behind the screen:</strong> place an order in the app, then check the database. Is the order saved with the right total and status, or did the screen just <em>show</em> success?</li>
<li><strong>Go around the UI:</strong> the app only lets customers choose quantities 1–10, but what does the API accept if you send 1,000 or -5 directly? Knowing there is an API makes this test possible.</li>
<li><strong>Use the logs:</strong> trigger a failed payment and check the server logs for a clear error, with no card details written in plain text.</li>
<li><strong>Integration points:</strong> knowing that orders are passed to the restaurant through a message queue, test what happens when that queue is slow or down.</li>
</ul>
<p>Grey-box testing is common in <strong>integration</strong>, <strong>API</strong> and <strong>security</strong> testing, where knowing a little about the inside finds bugs a pure black-box tester would never reach.</p>
<h2>Experience-based techniques</h2>
<p>Alongside the three boxes, testers rely on experience:</p>
<ul>
<li><strong>Error guessing:</strong> &quot;what usually breaks?&quot; Empty fields, very long text, special characters, double clicks, the back button, a lost network.</li>
<li><strong>Exploratory testing:</strong> learning, designing and testing at the same time, guided by curiosity and a time limit.</li>
<li><strong>Checklists:</strong> a team&#39;s growing list of things that have gone wrong before.</li>
</ul>
<h2>Comparing the three</h2>
<table>
<thead>
<tr>
<th></th>
<th>Black-box</th>
<th>Grey-box</th>
<th>White-box</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Sees</strong></td>
<td>Inputs and outputs</td>
<td>Some internals</td>
<td>The code</td>
</tr>
<tr>
<td><strong>Based on</strong></td>
<td>Requirements</td>
<td>Requirements + architecture</td>
<td>Code structure</td>
</tr>
<tr>
<td><strong>Usually done by</strong></td>
<td>QA testers</td>
<td>QA, automation, security</td>
<td>Developers</td>
</tr>
<tr>
<td><strong>Typical levels</strong></td>
<td>System, acceptance</td>
<td>Integration, API, security</td>
<td>Unit</td>
</tr>
<tr>
<td><strong>Finds</strong></td>
<td>Missing or wrong behaviour</td>
<td>Hidden data and integration problems</td>
<td>Untested logic, dead code</td>
</tr>
<tr>
<td><strong>Misses</strong></td>
<td>Untested code paths</td>
<td>Very deep logic</td>
<td>Missing features (you can&#39;t test code that was never written)</td>
</tr>
</tbody></table>
<p>The last row matters most: <strong>each approach misses what the others catch</strong>. White-box testing can&#39;t find a feature nobody built, and black-box testing can&#39;t see a branch nobody reached. Good teams use all three.</p>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/how-to-write-a-test-plan/">Part 8</a></strong> we put it together in a <strong>test plan</strong>: what to test, how, by whom and when.</p>
<p>ProvaLab helps on every side of the box: Test Management keeps your black-box test cases, the coding agent writes and runs unit tests from the code, and API and security testing reach behind the UI. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The bug life cycle explained: from New to Closed (and Reopened)</title>
      <link>https://provalab.io/blog/bug-life-cycle/</link>
      <guid isPermaLink="true">https://provalab.io/blog/bug-life-cycle/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>bug-life-cycle</category>
      <description>Every status a bug passes through, who moves it at each step, the side exits like Duplicate and Deferred, one bug followed from start to finish, and the habits that keep a bug tracker healthy.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a> you wrote a great bug report. Now what? A bug doesn&#39;t get fixed just because it was reported. It travels through a series of <strong>statuses</strong>, passing between testers, leads and developers, until it is finally closed.</p>
<p>That journey is the <strong>bug life cycle</strong>, also called the <strong>defect life cycle</strong>. Knowing it well means fewer bugs get lost, and nobody wonders &quot;wait, who has this one?&quot;</p>
<h2>The bug life cycle at a glance</h2>
<p><img src="https://provalab.io/blog/basics/bug-life-cycle.svg" alt="The bug life cycle. Main path: New, Assigned, Open, Fixed, Ready to retest, Retest, Verified, Closed. If the retest fails the bug is Reopened and goes back to Assigned. Other endings from New: Rejected (not a bug), Duplicate (already reported), Deferred (fix in a later release) and Need more info (can't reproduce)." width="880" height="520" loading="lazy"></p>
<h2>Every status, one by one</h2>
<h3>New</h3>
<p>A tester (or a user, or an automated test) has <strong>reported</strong> the bug. Nobody has looked at it yet. This is where every bug starts.</p>
<h3>Assigned</h3>
<p>A test lead, product owner or developer lead has <strong>triaged</strong> it: confirmed it&#39;s a real bug, set its priority, and assigned it to a developer.</p>
<h3>Open (or In Progress)</h3>
<p>The developer has <strong>started working on it</strong>: reproducing it, finding the cause, writing the fix.</p>
<h3>Fixed</h3>
<p>The developer has <strong>changed the code</strong> and believes the bug is solved. Good practice is to also add an automated test that would have caught it, so it can&#39;t quietly come back.</p>
<h3>Ready to retest</h3>
<p>The fix has been <strong>deployed to the test environment</strong>, and the bug is waiting for a tester. A fix that only exists on the developer&#39;s laptop can&#39;t be retested yet. This status makes that clear.</p>
<h3>Retest</h3>
<p>The tester <strong>follows the original steps</strong> from the bug report again, on the new build.</p>
<h3>Verified</h3>
<p>The retest <strong>passed</strong>: the bug is gone. The tester also runs a few <strong>regression tests</strong> around it, to make sure the fix didn&#39;t break something nearby.</p>
<h3>Closed</h3>
<p>The bug is <strong>done</strong>. Usually the tester closes it after verifying, or it closes automatically when the fix is released.</p>
<h3>Reopened</h3>
<p>The retest <strong>failed</strong>: the bug is still there, or it only went away in some cases. The tester reopens it with a comment on what still happens, and it goes back to the developer.</p>
<h2>The side exits</h2>
<p>Not every bug follows the main path. During triage, a bug can also end up as:</p>
<table>
<thead>
<tr>
<th>Status</th>
<th>Meaning</th>
<th>What the tester should do</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Rejected / Not a bug</strong></td>
<td>The software is working as intended</td>
<td>Check the requirement. If the behaviour is still wrong for users, raise it as a change request instead</td>
</tr>
<tr>
<td><strong>Duplicate</strong></td>
<td>The same bug is already reported</td>
<td>Link to the original; add any new details there</td>
</tr>
<tr>
<td><strong>Deferred / Postponed</strong></td>
<td>Real, but will be fixed in a later release</td>
<td>Make sure the risk is understood and the bug isn&#39;t forgotten</td>
</tr>
<tr>
<td><strong>Need more info / Can&#39;t reproduce</strong></td>
<td>The developer can&#39;t make it happen</td>
<td>Add more detail: exact data, environment, a screen recording</td>
</tr>
</tbody></table>
<p><strong>&quot;Can&#39;t reproduce&quot; is usually a sign the report was missing something</strong>, which is why the habits in Part 9 matter so much.</p>
<h2>Who moves the bug?</h2>
<p>Each status change belongs to someone. If nobody owns a step, bugs get stuck there:</p>
<p><img src="https://provalab.io/blog/basics/bug-roles.svg" alt="Who moves the bug. The tester reports it as New. The lead or product owner triages it to Assigned, or to Rejected, Duplicate or Deferred. The developer moves it from Open to Fixed. The tester retests it and marks it Verified and Closed, or Reopened." width="880" height="330" loading="lazy"></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>Usually done by</th>
</tr>
</thead>
<tbody><tr>
<td>New → Assigned (or Rejected, Duplicate, Deferred)</td>
<td>Test lead, product owner or developer lead</td>
</tr>
<tr>
<td>Assigned → Open → Fixed</td>
<td>Developer</td>
</tr>
<tr>
<td>Fixed → Ready to retest</td>
<td>Developer or the release process</td>
</tr>
<tr>
<td>Retest → Verified → Closed, or Reopened</td>
<td>Tester</td>
</tr>
</tbody></table>
<p><strong>The rule worth remembering: the person who found the bug should be the one who verifies the fix.</strong> They know exactly what was wrong.</p>
<h2>One bug, start to finish</h2>
<p>Let&#39;s follow <strong>BUG-231</strong> from Part 9, &quot;Expired discount code accepted at checkout&quot;:</p>
<ol>
<li><strong>New</strong> (Mon 10:05): the tester reports it, with steps, a recording and a link to test case TC-104.</li>
<li><strong>Assigned</strong> (Mon 11:30): the team lead sets priority High, because it costs money, and assigns it to a developer.</li>
<li><strong>Open</strong> (Mon 14:00): the developer finds that the code checks the start date of a discount but never the end date.</li>
<li><strong>Fixed</strong> (Mon 16:20): the developer adds the missing check and an automated test for an expired code.</li>
<li><strong>Ready to retest</strong> (Tue 09:00): the fix is deployed to staging.</li>
<li><strong>Retest → Reopened</strong> (Tue 10:15): OLD10 is now refused on the web, but <strong>still accepted in the Android app</strong>, which has its own copy of the check. The tester reopens the bug with that detail.</li>
<li><strong>Fixed again</strong> (Tue 15:00): the check moves to the server, so every app uses the same rule.</li>
<li><strong>Retest → Verified</strong> (Wed 09:30): refused on web and Android; regression tests for valid codes still pass.</li>
<li><strong>Closed</strong> (Wed 09:35).</li>
</ol>
<p>The reopen in step 6 is not a failure of the process. <strong>It is the process working.</strong> Without a proper retest, half the bug would have shipped.</p>
<h2>Keeping the bug tracker healthy</h2>
<ul>
<li><strong>Triage regularly</strong>, ideally every day in busy periods, so no bug sits in &quot;New&quot; for a week.</li>
<li><strong>Watch for stuck bugs.</strong> A bug in &quot;Fixed&quot; for days probably never reached the test environment.</li>
<li><strong>Watch the reopen rate.</strong> If many bugs come back, fixes are being rushed or retests skipped.</li>
<li><strong>Don&#39;t let &quot;Deferred&quot; become a graveyard.</strong> Review deferred bugs before each release.</li>
<li><strong>Comment on every status change.</strong> &quot;Reopened: still accepted in Android 2.4.0&quot; is far more useful than a silent change.</li>
</ul>
<h2>Your tool&#39;s names may differ</h2>
<p>Jira, Azure DevOps, GitHub Issues and other trackers each use their own names and allow teams to customise the flow. Some keep it as simple as <strong>To Do → In Progress → Done</strong>. The ideas stay the same: someone reports it, someone decides, someone fixes, someone checks, and only then is it closed.</p>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/requirements-traceability-matrix/">Part 11</a></strong> we connect everything with the <strong>requirements traceability matrix</strong>: the table that proves every requirement has tests, and shows which ones are failing.</p>
<p>In ProvaLab, issues follow this life cycle with their status, priority and assignee, are linked to the test case and test run that found them, and can be raised straight from a failed automated test. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Exploratory testing explained: charters, sessions and heuristics</title>
      <link>https://provalab.io/blog/exploratory-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/exploratory-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>exploratory-testing</category>
      <description>What exploratory testing is and isn't, how session-based testing works, how to write a charter, the heuristics skilled testers use to generate ideas, and how to report what you found.</description>
      <content:encoded><![CDATA[<p>Every scripted test in this series checks something somebody already thought of. But the bug that doubles a discount when you change the delivery address and press back? Nobody wrote a test case for that, because nobody imagined it.</p>
<p><strong>Exploratory testing</strong> is how skilled testers find those bugs. It&#39;s the most misunderstood kind of testing: often dismissed as &quot;just clicking around&quot;, when done well it is one of the most effective techniques there is.</p>
<h2>What is exploratory testing?</h2>
<p><strong>In exploratory testing, the tester designs, runs and learns from tests at the same time</strong>, using what they just discovered to decide what to try next.</p>
<p>In scripted testing, the thinking happens first (writing the test case) and the running happens later, often by someone else. In exploratory testing the two happen together: you try something, see what the software does, form a new idea, and try that.</p>
<p>Three things make it different from &quot;just clicking around&quot;:</p>
<ol>
<li><strong>It has a goal.</strong> A written <strong>charter</strong> says what to explore and why.</li>
<li><strong>It is time-boxed.</strong> A <strong>session</strong> lasts a fixed time, typically 60 to 120 minutes.</li>
<li><strong>It is recorded.</strong> The tester keeps notes of what they tried, what they found and what questions came up, and reports them afterwards.</li>
</ol>
<p>This structured form is called <strong>session-based test management</strong>, developed by James and Jonathan Bach.</p>
<h2>Scripted vs exploratory testing</h2>
<p><img src="https://provalab.io/blog/basics/scripted-vs-exploratory.svg" alt="Two overlapping circles. Scripted testing finds known risks: requirements not met, regressions, the same checks every release. Exploratory testing finds the unknown: unexpected sequences, gaps the spec never mentioned, usability problems. Core features sit in the overlap. Scripts guard what you know; exploration finds what you don't." width="880" height="400" loading="lazy"></p>
<table>
<thead>
<tr>
<th></th>
<th>Scripted testing</th>
<th>Exploratory testing</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Tests designed</strong></td>
<td>In advance, from requirements</td>
<td>During the session, from what you learn</td>
</tr>
<tr>
<td><strong>Best at finding</strong></td>
<td>Requirements not met, regressions</td>
<td>Unexpected behaviour, gaps in the requirements, usability problems</td>
</tr>
<tr>
<td><strong>Repeatable?</strong></td>
<td>Yes, by anyone</td>
<td>Partly: notes record what was tried</td>
</tr>
<tr>
<td><strong>Depends on</strong></td>
<td>Good requirements</td>
<td>Tester skill and curiosity</td>
</tr>
<tr>
<td><strong>Good for automation?</strong></td>
<td>Yes</td>
<td>No, by definition</td>
</tr>
<tr>
<td><strong>Coverage</strong></td>
<td>Known, measurable</td>
<td>Harder to measure; charters track it</td>
</tr>
</tbody></table>
<p>They are not rivals. Scripted tests, especially <a href="https://provalab.io/blog/regression-testing-vs-retesting/">automated regression tests</a>, guard against what you already know can go wrong. Exploratory testing finds what you don&#39;t know yet. <a href="https://provalab.io/blog/seven-principles-of-testing/">Principle 5</a>, tests wear out, is exactly why you need both.</p>
<h2>A session, start to finish</h2>
<p><img src="https://provalab.io/blog/basics/exploratory-session.svg" alt="A session-based exploratory test. The charter: explore discount codes at checkout with unusual sequences and inputs to discover ways the discount can be wrong. The tester's notes have three columns: ideas tried (apply then change address and go back, lower-case code, trailing space, apply twice quickly, remove all items, switch language), bugs found (discount applied twice after going back; trailing space gives &quot;code not recognised&quot; with no hint) and questions (should codes be case-insensitive? what if a code expires during checkout? Android not covered). The debrief after 75 minutes: 2 bugs, 2 questions, next charter the same on Android." width="880" height="484" loading="lazy"></p>
<h3>1. Write the charter</h3>
<p>A charter is one or two sentences describing the mission. A useful template, from Elisabeth Hendrickson&#39;s <em>Explore It!</em>:</p>
<blockquote>
<p><strong>Explore</strong> <em>(the target)</em> <strong>with</strong> <em>(resources or approaches)</em> <strong>to discover</strong> <em>(the information you want)</em>.</p>
</blockquote>
<p>For our food delivery app:</p>
<blockquote>
<p>Explore <strong>discount codes at checkout</strong> with <strong>unusual sequences and inputs</strong> to discover <strong>ways the discount can be wrong</strong>.</p>
</blockquote>
<p>A charter is narrow enough to finish in a session and open enough to leave room for discovery. &quot;Test checkout&quot; is too broad; &quot;apply code SAVE10&quot; is a test case, not a charter.</p>
<h3>2. Run the session</h3>
<p>Set a timer. Start from the charter, and let each result suggest the next idea. Keep notes as you go, in three groups:</p>
<ul>
<li><strong>Ideas tried:</strong> so you and others know what was covered.</li>
<li><strong>Bugs found:</strong> with enough detail to <a href="https://provalab.io/blog/how-to-write-a-bug-report/">write a proper bug report</a> afterwards.</li>
<li><strong>Questions:</strong> things the requirements don&#39;t answer, and areas you didn&#39;t reach.</li>
</ul>
<p>When something looks wrong, <strong>stop and reproduce it</strong> before moving on. A bug you can&#39;t reproduce is a bug you&#39;ll struggle to report.</p>
<h3>3. Debrief</h3>
<p>At the end, report briefly: what was covered, what was found, what&#39;s left. Then decide the next charter. In the example, the discount bugs on the web suggest an obvious follow-up: the same charter on Android.</p>
<h2>Heuristics: where ideas come from</h2>
<p>Experienced testers don&#39;t rely on inspiration. They use <strong>heuristics</strong>: rules of thumb that generate test ideas on demand. A few of the most useful:</p>
<p><strong>Vary the input</strong></p>
<ul>
<li><strong>Boundaries:</strong> zero, one, the maximum, one more than the maximum (see <a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>).</li>
<li><strong>Nothing:</strong> empty fields, missing files, no network.</li>
<li><strong>Too much:</strong> very long text, huge files, thousands of items.</li>
<li><strong>Strange characters:</strong> spaces at the ends, emoji, quotes, <code>&lt;script&gt;</code>, text in another alphabet.</li>
<li><strong>Wrong type:</strong> letters in a number field, a date in the past.</li>
</ul>
<p><strong>Vary the sequence</strong></p>
<ul>
<li>Do things in the wrong order. Pay, then add an item.</li>
<li><strong>Interrupt:</strong> go back, refresh, lose the network, close the app mid-way.</li>
<li><strong>Repeat:</strong> click twice, submit twice, apply the same code twice.</li>
<li><strong>Undo:</strong> add then remove, apply then cancel.</li>
</ul>
<p><strong>Vary the state and environment</strong></p>
<ul>
<li>A new user vs a user with 500 orders.</li>
<li>Two browser tabs doing the same thing at once.</li>
<li>Switch language, time zone or currency mid-flow.</li>
<li>Different devices, screen sizes, slow connections.</li>
</ul>
<p><strong>Follow the data</strong></p>
<ul>
<li>Where does this value go after I enter it? Check the order history, the email, the restaurant&#39;s screen. Is it the same everywhere?</li>
</ul>
<p><strong>Think like different people</strong></p>
<ul>
<li>A first-time user who misreads the screen. An impatient user. A restaurant owner. Someone trying to get a discount they shouldn&#39;t.</li>
</ul>
<p>Keeping a personal list of heuristics, and adding to it every time a new kind of bug turns up, is one of the fastest ways to become a better tester.</p>
<h2>When to use exploratory testing</h2>
<ul>
<li><strong>New features</strong>, before test cases exist or while they&#39;re being written.</li>
<li><strong>Right after a sprint&#39;s work lands</strong>, to find problems the stories didn&#39;t anticipate.</li>
<li><strong>Around a bug fix</strong>, alongside the <a href="https://provalab.io/blog/regression-testing-vs-retesting/">re-test and regression run</a>.</li>
<li><strong>Before a release</strong>, on the riskiest areas, after the scripted suite has passed.</li>
<li><strong>When requirements are thin</strong> and scripted tests would mostly be guesses.</li>
<li><strong>For usability</strong>: scripts can&#39;t tell you a flow is confusing.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>No charter.</strong> Aimless clicking covers the easy paths and misses the rest.</li>
<li><strong>No notes.</strong> If nobody knows what was explored, the session can&#39;t be reported, repeated or built on.</li>
<li><strong>Only happy paths.</strong> The value is in the odd sequences and inputs.</li>
<li><strong>Treating it as a replacement for regression testing.</strong> Exploration doesn&#39;t guard against old bugs returning; automated scripts do.</li>
<li><strong>Only junior testers do it.</strong> It benefits most from experience; put your most knowledgeable people on the riskiest charters.</li>
<li><strong>Not reporting bugs properly.</strong> A finding in your notes isn&#39;t fixed until it&#39;s a <a href="https://provalab.io/blog/how-to-write-a-bug-report/">bug report</a> in the tracker.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Part 23</a></strong>, the last part of Testing Basics, we look at <strong>testing in agile teams and CI/CD pipelines</strong>: how everything in this series fits together when the team ships every week, or every day.</p>
<p>ProvaLab keeps exploratory findings next to scripted results: a bug found in a session is filed with its steps and evidence, linked to the feature, and visible alongside the automated runs for the same release. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your first API tests in 15 minutes (Python, pytest and requests)</title>
      <link>https://provalab.io/blog/first-api-test-pytest/</link>
      <guid isPermaLink="true">https://provalab.io/blog/first-api-test-pytest/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>api-testing</category>
      <category>pytest</category>
      <category>fastapi</category>
      <description>Build a tiny discount API you own, then write eleven pytest tests against it: functional, contract, negative, authentication and authorisation checks, with the real output and a planted bug.</description>
      <content:encoded><![CDATA[<p><a href="https://provalab.io/blog/api-testing-explained/">Part 3</a> explained what to check in an API response. Now we check it. Rather than test someone else&#39;s API, which you may not have permission to hit, we&#39;ll build a tiny one of our own in about forty lines, then write a proper test suite against it: functional, contract, negative, authentication and authorisation tests.</p>
<p>Every command and every line of output below was run while writing this post.</p>
<p><strong>What you need:</strong> Python 3.10 or newer and a terminal. Nothing to sign up for.</p>
<h2>Step 1: Set up (2 minutes)</h2>
<pre><code class="language-bash">mkdir first-api-test
cd first-api-test
python3 -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\Activate.ps1
pip install fastapi uvicorn requests pytest
</code></pre>
<p><strong>FastAPI</strong> builds the API, <strong>uvicorn</strong> runs it, <strong>requests</strong> calls it, <strong>pytest</strong> runs the tests.</p>
<h2>Step 2: Build the API (5 minutes)</h2>
<p>Create <code>app.py</code>. It&#39;s the discount feature from our food delivery app, as an API:</p>
<pre><code class="language-python">from datetime import date

from fastapi import Depends, FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title=&quot;Discount API&quot;)

# Who is who: a bearer token maps to a customer (a real API would check a database)
TOKENS = {&quot;token-alice&quot;: &quot;alice&quot;, &quot;token-bob&quot;: &quot;bob&quot;}

# Discount codes: percentage off and the day they expire
CODES = {
    &quot;SAVE10&quot;: {&quot;percent&quot;: 10, &quot;expires&quot;: date(2099, 1, 1)},
    &quot;OLD10&quot;: {&quot;percent&quot;: 10, &quot;expires&quot;: date(2025, 11, 1)},
}

# Orders and who owns them
ORDERS = {42: {&quot;customer&quot;: &quot;alice&quot;, &quot;total&quot;: 500}, 43: {&quot;customer&quot;: &quot;bob&quot;, &quot;total&quot;: 300}}


def current_customer(authorization: str = Header(default=&quot;&quot;)) -&gt; str:
    token = authorization.removeprefix(&quot;Bearer &quot;).strip()
    if token not in TOKENS:
        raise HTTPException(status_code=401, detail=&quot;sign in first&quot;)
    return TOKENS[token]


class ApplyDiscount(BaseModel):
    order_total: float = Field(gt=0)
    code: str


@app.post(&quot;/discounts/apply&quot;)
def apply_discount(body: ApplyDiscount, customer: str = Depends(current_customer)):
    code = CODES.get(body.code.upper())
    if code is None:
        raise HTTPException(status_code=404, detail=&quot;code not recognised&quot;)
    if code[&quot;expires&quot;] &lt; date.today():
        raise HTTPException(status_code=400, detail=&quot;this code has expired&quot;)
    discount = body.order_total * code[&quot;percent&quot;] / 100
    return {&quot;total&quot;: body.order_total - discount, &quot;discount&quot;: discount, &quot;code&quot;: body.code.upper()}


@app.get(&quot;/orders/{order_id}&quot;)
def get_order(order_id: int, customer: str = Depends(current_customer)):
    order = ORDERS.get(order_id)
    if order is None:
        raise HTTPException(status_code=404, detail=&quot;order not found&quot;)
    if order[&quot;customer&quot;] != customer:
        raise HTTPException(status_code=403, detail=&quot;not your order&quot;)
    return {&quot;id&quot;: order_id, **order}
</code></pre>
<p>Two endpoints:</p>
<ul>
<li><code>POST /discounts/apply</code> takes an order total and a code, and returns the discounted total. Unknown codes get <code>404</code>, expired ones <code>400</code>, and a total of zero or less is rejected with <code>422</code> (FastAPI validates the body for us, because of <code>Field(gt=0)</code>).</li>
<li><code>GET /orders/{id}</code> returns an order, but only to the customer who owns it: <code>401</code> with no valid token, <code>403</code> for someone else&#39;s order, <code>404</code> if it doesn&#39;t exist.</li>
</ul>
<p>Start it in a terminal and leave it running:</p>
<pre><code class="language-bash">uvicorn app:app
</code></pre>
<p>Then make one request by hand, to see what a response looks like:</p>
<pre><code class="language-bash">curl -i -X POST http://127.0.0.1:8000/discounts/apply \
  -H &quot;Authorization: Bearer token-alice&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;}&#39;
</code></pre>
<pre><code class="language-text">HTTP/1.1 200 OK
content-length: 47
content-type: application/json

{&quot;total&quot;:450.0,&quot;discount&quot;:50.0,&quot;code&quot;:&quot;SAVE10&quot;}
</code></pre>
<p>Status, headers, body: the three things from Part 3, in that order.</p>
<h2>Step 3: Fixtures, so every test doesn&#39;t repeat itself (2 minutes)</h2>
<p>Open a <strong>second terminal</strong> in the same folder, with the virtual environment activated. Create <code>conftest.py</code>, where pytest looks for shared setup:</p>
<pre><code class="language-python">import os

import pytest
import requests

BASE_URL = os.environ.get(&quot;API_URL&quot;, &quot;http://127.0.0.1:8000&quot;)


@pytest.fixture
def api():
    &quot;&quot;&quot;A session signed in as Alice. Every request through it carries her token.&quot;&quot;&quot;
    session = requests.Session()
    session.headers[&quot;Authorization&quot;] = &quot;Bearer token-alice&quot;
    session.base = BASE_URL
    yield session
    session.close()


@pytest.fixture
def anonymous():
    &quot;&quot;&quot;A session with no token at all.&quot;&quot;&quot;
    session = requests.Session()
    session.base = BASE_URL
    yield session
    session.close()
</code></pre>
<p>A <strong>fixture</strong> is setup that a test asks for by name. <code>api</code> gives a session already signed in as Alice; <code>anonymous</code> gives one with no token. The address comes from an environment variable, so the same tests can later point at a staging server: <code>API_URL=https://staging.example.com pytest</code>.</p>
<h2>Step 4: The discount tests (3 minutes)</h2>
<p>Create <code>test_discounts.py</code>:</p>
<pre><code class="language-python">def test_valid_code_reduces_the_total(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;})
    assert r.status_code == 200
    assert r.json() == {&quot;total&quot;: 450, &quot;discount&quot;: 50, &quot;code&quot;: &quot;SAVE10&quot;}


def test_response_matches_the_contract(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;})
    body = r.json()
    assert r.headers[&quot;Content-Type&quot;] == &quot;application/json&quot;
    assert set(body) == {&quot;total&quot;, &quot;discount&quot;, &quot;code&quot;}
    assert isinstance(body[&quot;total&quot;], (int, float))
    assert isinstance(body[&quot;discount&quot;], (int, float))
    assert isinstance(body[&quot;code&quot;], str)
    assert r.elapsed.total_seconds() &lt; 0.5


def test_code_is_not_case_sensitive(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;save10&quot;})
    assert r.status_code == 200
    assert r.json()[&quot;total&quot;] == 450


def test_unknown_code_is_refused(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;NOPE99&quot;})
    assert r.status_code == 404
    assert r.json()[&quot;detail&quot;] == &quot;code not recognised&quot;


def test_expired_code_is_refused(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;OLD10&quot;})
    assert r.status_code == 400
    assert &quot;expired&quot; in r.json()[&quot;detail&quot;]


def test_zero_or_negative_total_is_rejected(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 0, &quot;code&quot;: &quot;SAVE10&quot;})
    assert r.status_code == 422


def test_missing_field_is_rejected(api):
    r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500})
    assert r.status_code == 422
</code></pre>
<p>Notice how the five checks from Part 3 show up:</p>
<ul>
<li><strong>Status code</strong> in every test, and the <em>exact</em> one: <code>404</code> for an unknown code, <code>400</code> for an expired one, <code>422</code> for bad input. Not &quot;some 4xx&quot;.</li>
<li><strong>Headers and body shape</strong> in <code>test_response_matches_the_contract</code>: the content type, exactly the three promised fields, the right types. This is the test that catches a renamed field before it breaks the mobile app.</li>
<li><strong>Values</strong> in the first test: 500 with <code>SAVE10</code> is 450, full stop.</li>
<li><strong>Timing</strong> with <code>r.elapsed</code>: under half a second.</li>
<li><strong>Negative cases</strong> are four of the seven tests. That&#39;s roughly the right proportion.</li>
</ul>
<h2>Step 5: The authorisation tests (2 minutes)</h2>
<p>Create <code>test_auth.py</code>:</p>
<pre><code class="language-python">def test_no_token_means_401(anonymous):
    r = anonymous.post(f&quot;{anonymous.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;})
    assert r.status_code == 401


def test_customer_can_read_their_own_order(api):
    r = api.get(f&quot;{api.base}/orders/42&quot;)
    assert r.status_code == 200
    assert r.json()[&quot;customer&quot;] == &quot;alice&quot;


def test_customer_cannot_read_someone_elses_order(api):
    r = api.get(f&quot;{api.base}/orders/43&quot;)  # Bob&#39;s order, Alice&#39;s token
    assert r.status_code == 403


def test_unknown_order_is_404(api):
    r = api.get(f&quot;{api.base}/orders/999&quot;)
    assert r.status_code == 404
</code></pre>
<p><img src="https://provalab.io/blog/handson/auth-ladder.svg" alt="Four requests for GET /orders/43 and their correct answers. No token: 401 Unauthorized, authentication failed. Alice's token for Bob's order: 403 Forbidden, authorisation failed, the bug to look for. Bob's token: 200 with the order. Any token for order 999: 404 Not Found. Assert the exact code: a test accepting any 4xx would miss an API that returns 404 instead of 403." width="880" height="400" loading="lazy"></p>
<p><code>test_customer_cannot_read_someone_elses_order</code> is the most valuable test in this post. Signing in as one customer and asking for another&#39;s data is the single most common serious API security bug, and it takes four lines to check. Note the exact code: <code>403</code>, not &quot;any error&quot;. An API that answered <code>404</code> here would be quietly telling an attacker that order 43 exists.</p>
<h2>Step 6: Run it (1 minute)</h2>
<pre><code class="language-bash">pytest -v
</code></pre>
<pre><code class="language-text">collected 11 items

test_auth.py::test_no_token_means_401 PASSED                             [  9%]
test_auth.py::test_customer_can_read_their_own_order PASSED              [ 18%]
test_auth.py::test_customer_cannot_read_someone_elses_order PASSED       [ 27%]
test_auth.py::test_unknown_order_is_404 PASSED                           [ 36%]
test_discounts.py::test_valid_code_reduces_the_total PASSED              [ 45%]
test_discounts.py::test_response_matches_the_contract PASSED             [ 54%]
test_discounts.py::test_code_is_not_case_sensitive PASSED                [ 63%]
test_discounts.py::test_unknown_code_is_refused PASSED                   [ 72%]
test_discounts.py::test_expired_code_is_refused PASSED                   [ 81%]
test_discounts.py::test_zero_or_negative_total_is_rejected PASSED        [ 90%]
test_discounts.py::test_missing_field_is_rejected PASSED                 [100%]

============================== 11 passed in 0.08s ==============================
</code></pre>
<p><strong>Eleven tests, 0.08 seconds.</strong> The three browser tests in <a href="https://provalab.io/blog/first-playwright-test/">Part 2</a> took 10.68 seconds. That&#39;s the speed difference from Part 3 made concrete: about a hundred times faster per test.</p>
<p><img src="https://provalab.io/blog/handson/api-suite-map.svg" alt="The eleven tests grouped by kind. Functional, three: valid code reduces the total, code is not case-sensitive, customer can read their own order. Contract, one: exact fields, right types, under 500 ms. Negative, four: unknown code 404, expired 400, zero total 422, missing field 422. Auth, three: no token 401, someone else's order 403, unknown order 404. All in 0.08 seconds. The planted bug broke exactly the two that check values." width="880" height="470" loading="lazy"></p>
<h2>Step 7: Plant a bug (2 minutes)</h2>
<p>Change one line in <code>app.py</code> so the discount is subtracted as a flat amount instead of a percentage:</p>
<pre><code class="language-python">    discount = code[&quot;percent&quot;]  # bug: flat amount, not a percentage
</code></pre>
<p>Restart <code>uvicorn</code> and run the tests again:</p>
<pre><code class="language-text">....F.F....                                                              [100%]
=================================== FAILURES ===================================
______________________ test_valid_code_reduces_the_total _______________________

    def test_valid_code_reduces_the_total(api):
        r = api.post(f&quot;{api.base}/discounts/apply&quot;, json={&quot;order_total&quot;: 500, &quot;code&quot;: &quot;SAVE10&quot;})
        assert r.status_code == 200
&gt;       assert r.json() == {&quot;total&quot;: 450, &quot;discount&quot;: 50, &quot;code&quot;: &quot;SAVE10&quot;}
E       AssertionError: assert {&#39;total&#39;: 490...de&#39;: &#39;SAVE10&#39;} == {&#39;total&#39;: 450...de&#39;: &#39;SAVE10&#39;}
E         Differing items:
E         {&#39;discount&#39;: 10} != {&#39;discount&#39;: 50}
E         {&#39;total&#39;: 490.0} != {&#39;total&#39;: 450}

test_discounts.py:4: AssertionError
_______________________ test_code_is_not_case_sensitive ________________________
&gt;       assert r.json()[&quot;total&quot;] == 450
E       assert 490.0 == 450

test_discounts.py:21: AssertionError
========================= short test summary info ============================
FAILED test_discounts.py::test_valid_code_reduces_the_total - AssertionError:...
FAILED test_discounts.py::test_code_is_not_case_sensitive - assert 490.0 == 450
2 failed, 9 passed in 0.06s
</code></pre>
<p>Exactly the two tests that check <strong>values</strong> fail; the nine that check status codes, shape and permissions still pass, because the bug didn&#39;t touch those. pytest&#39;s diff says precisely what&#39;s wrong: <code>discount 10 != 50</code>. Put the line back, and you&#39;re green again.</p>
<h2>What this suite doesn&#39;t cover, and what to add next</h2>
<ul>
<li><strong>A sequence:</strong> create an order, apply a code, read it back. That&#39;s <a href="https://provalab.io/blog/integration-testing/">integration testing</a> through the API.</li>
<li><strong>Contract from a document:</strong> FastAPI generates an OpenAPI document at <code>/openapi.json</code>; tools like Schemathesis can generate hundreds of tests from it.</li>
<li><strong>Load:</strong> the same endpoints under 200 users at once, as described in <a href="https://provalab.io/blog/performance-testing-types/">performance testing types</a>; we build one with k6 later in this series.</li>
<li><strong>Running in CI:</strong> the same <code>pytest</code> step as in Part 2&#39;s workflow, with <code>uvicorn app:app &amp;</code> started first, or a test server fixture.</li>
</ul>
<h2>Habits worth keeping</h2>
<ol>
<li><strong>Assert the exact status code.</strong> The difference between 401, 403 and 404 is the whole point.</li>
<li><strong>Test the contract separately from the values.</strong> Shape changes break clients even when the maths is right.</li>
<li><strong>Make negative tests at least a third of the suite.</strong> That&#39;s where the bugs live.</li>
<li><strong>Always test &quot;someone else&#39;s data&quot;.</strong> One test, four lines, the most common serious API bug.</li>
<li><strong>Never put real tokens in the code.</strong> Read them from the environment; use test accounts.</li>
</ol>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/security-testing-basics-owasp-top-10/">Part 5</a></strong> we move to <strong>security testing</strong>: the OWASP Top 10 in plain language, and which of those risks the tests you just wrote already touch.</p>
<p>ProvaLab&#39;s QA AI Agent runs API test cases like these on its own and records every request and response against its test case; AppSec&#39;s API fuzzing sends thousands of malformed and unauthorised requests to find the 403-that-should-have-been and the 500-that-should-have-been-a-400. See it in the <a href="https://provalab.io/#stage-agent">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your first automated test in 15 minutes (Python + pytest)</title>
      <link>https://provalab.io/blog/first-automated-test-pytest/</link>
      <guid isPermaLink="true">https://provalab.io/blog/first-automated-test-pytest/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-automation</category>
      <category>pytest</category>
      <description>A hands-on, step-by-step guide to writing and running your first automated tests with Python and pytest, including what a failing test looks like and how to read it.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/manual-vs-automated-testing/">Part 3</a> we saw why teams automate the tests they run again and again. Now let&#39;s write some. By the end of this post you will have <strong>eight automated tests running in under a second</strong>, and you will know how to read a failing one.</p>
<p>We will use <strong>Python</strong> and <strong>pytest</strong>, the most popular way to write tests in Python. pytest needs almost no setup, and its tests are plain Python functions.</p>
<p><strong>What you need:</strong> Python 3.10 or newer, and a terminal. Check your version with <code>python3 --version</code> (on Windows: <code>python --version</code>). No testing experience required.</p>
<h2>Step 1: Set up a project (2 minutes)</h2>
<p>Create a folder and a <strong>virtual environment</strong>: a private space for this project&#39;s Python packages, so they don&#39;t mix with anything else on your computer.</p>
<pre><code class="language-bash">mkdir first-test
cd first-test
python3 -m venv .venv
</code></pre>
<p>Turn the environment on:</p>
<pre><code class="language-bash"># macOS and Linux
source .venv/bin/activate

# Windows (PowerShell)
.venv\Scripts\Activate.ps1
</code></pre>
<p>Your prompt now starts with <code>(.venv)</code>. Install pytest:</p>
<pre><code class="language-bash">pip install pytest
</code></pre>
<h2>Step 2: Write the code we want to test (2 minutes)</h2>
<p>We will keep using the <strong>food delivery app</strong> from earlier parts. Create a file called <code>shop.py</code> with one function that adds up an order and applies a discount:</p>
<pre><code class="language-python">def order_total(prices, discount_percent=0):
    &quot;&quot;&quot;Add up the prices and apply a percentage discount.&quot;&quot;&quot;
    if not 0 &lt;= discount_percent &lt;= 100:
        raise ValueError(&quot;discount must be between 0 and 100&quot;)
    subtotal = sum(prices)
    return subtotal - subtotal * discount_percent / 100
</code></pre>
<p>This is exactly the kind of small, important piece of code that <strong>unit tests</strong> (from <a href="https://provalab.io/blog/types-of-software-testing/">Part 2</a>) are made for: if the total is wrong, every customer pays the wrong price.</p>
<h2>Step 3: Write your first tests (3 minutes)</h2>
<p>Create a second file called <code>test_shop.py</code>:</p>
<pre><code class="language-python">from shop import order_total


def test_total_adds_up_prices():
    assert order_total([200, 200, 50]) == 450


def test_discount_is_applied():
    assert order_total([200, 200, 50], discount_percent=10) == 405


def test_empty_cart_is_zero():
    assert order_total([]) == 0
</code></pre>
<p>That&#39;s it. Three things to notice:</p>
<ul>
<li><strong>The file name starts with <code>test_</code></strong>, and so does each function. That is how pytest finds your tests automatically.</li>
<li><strong>Each test checks one thing</strong>, and its name says what. When it fails, the name alone tells you what broke.</li>
<li><strong><code>assert</code></strong> is plain Python: &quot;this must be true&quot;. If it isn&#39;t, the test fails.</li>
</ul>
<p>Each test follows the pattern from <a href="https://provalab.io/blog/what-is-software-testing/">Part 1</a>: a starting point (some prices), an action (call <code>order_total</code>) and an expected result (the number after <code>==</code>).</p>
<h2>Step 4: Run the tests (1 minute)</h2>
<pre><code class="language-bash">pytest -v
</code></pre>
<p>You should see:</p>
<pre><code class="language-text">collected 3 items

test_shop.py::test_total_adds_up_prices PASSED                           [ 33%]
test_shop.py::test_discount_is_applied PASSED                            [ 66%]
test_shop.py::test_empty_cart_is_zero PASSED                             [100%]

============================== 3 passed in 0.01s ===============================
</code></pre>
<p>Three tests, a hundredth of a second. The <code>-v</code> (verbose) flag lists each test by name; without it, pytest prints one dot per passing test.</p>
<h2>Step 5: Break something on purpose (3 minutes)</h2>
<p>A test you have never seen fail proves very little. Let&#39;s plant a bug. In <code>shop.py</code>, change the last line so the discount is subtracted as a flat amount instead of a percentage:</p>
<pre><code class="language-python">    return subtotal - discount_percent   # bug!
</code></pre>
<p>Run pytest again:</p>
<pre><code class="language-text">collected 3 items

test_shop.py .F.                                                         [100%]

=================================== FAILURES ===================================
___________________________ test_discount_is_applied ___________________________

    def test_discount_is_applied():
&gt;       assert order_total([200, 200, 50], discount_percent=10) == 405
E       assert 440 == 405
E        +  where 440 = order_total([200, 200, 50], discount_percent=10)

test_shop.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_shop.py::test_discount_is_applied - assert 440 == 405
========================= 1 failed, 2 passed in 0.02s ==========================
</code></pre>
<p>How to read it:</p>
<ul>
<li><strong><code>.F.</code></strong>: the first and third tests passed (<code>.</code>), the second failed (<code>F</code>).</li>
<li><strong>The <code>&gt;</code> line</strong> shows exactly which check failed.</li>
<li><strong><code>assert 440 == 405</code></strong>: the code returned <strong>440</strong>, but the test expected <strong>405</strong>. A 10% discount on 450 should be 45, not 10.</li>
<li><strong><code>test_shop.py:9</code></strong>: the file and line number to look at.</li>
</ul>
<p>The other two tests still pass, because the bug only affects discounts. That&#39;s why small, focused tests are so useful: the failing test&#39;s name points straight at the problem.</p>
<p>Now put the correct line back, run <code>pytest</code> again, and you&#39;re green.</p>
<h2>Step 6: Test many cases at once (2 minutes)</h2>
<p>Writing a new function for every number gets repetitive. pytest&#39;s <strong><code>parametrize</code></strong> runs one test with many inputs. Add this to the end of <code>test_shop.py</code>:</p>
<pre><code class="language-python">import pytest


@pytest.mark.parametrize(
    &quot;prices, discount, expected&quot;,
    [
        ([100], 0, 100),
        ([100], 50, 50),
        ([100], 100, 0),
        ([99, 1], 25, 75),
    ],
)
def test_order_totals(prices, discount, expected):
    assert order_total(prices, discount_percent=discount) == expected
</code></pre>
<p>Each row is its own test: if one row fails, you see exactly which inputs broke.</p>
<h2>Step 7: Test that bad input is refused (1 minute)</h2>
<p>Good tests check what the code <strong>shouldn&#39;t</strong> do, too. Our function must refuse a discount over 100%:</p>
<pre><code class="language-python">def test_discount_over_100_is_refused():
    with pytest.raises(ValueError):
        order_total([100], discount_percent=150)
</code></pre>
<p>This test <strong>passes only if</strong> the code raises a <code>ValueError</code>. If someone removes that check one day, this test will catch it.</p>
<p>Run everything:</p>
<pre><code class="language-text">collected 8 items

test_shop.py::test_total_adds_up_prices PASSED                           [ 12%]
test_shop.py::test_discount_is_applied PASSED                            [ 25%]
test_shop.py::test_empty_cart_is_zero PASSED                             [ 37%]
test_shop.py::test_order_totals[prices0-0-100] PASSED                    [ 50%]
test_shop.py::test_order_totals[prices1-50-50] PASSED                    [ 62%]
test_shop.py::test_order_totals[prices2-100-0] PASSED                    [ 75%]
test_shop.py::test_order_totals[prices3-25-75] PASSED                    [ 87%]
test_shop.py::test_discount_over_100_is_refused PASSED                   [100%]

============================== 8 passed in 0.01s ===============================
</code></pre>
<p><strong>Eight automated tests, done.</strong> Every time you change <code>shop.py</code>, one command tells you in a second whether anything broke.</p>
<h2>Handy pytest commands</h2>
<table>
<thead>
<tr>
<th>Command</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>pytest</code></td>
<td>Run all tests, one character per test</td>
</tr>
<tr>
<td><code>pytest -v</code></td>
<td>List every test by name</td>
</tr>
<tr>
<td><code>pytest -k discount</code></td>
<td>Run only tests whose name contains &quot;discount&quot;</td>
</tr>
<tr>
<td><code>pytest -x</code></td>
<td>Stop at the first failure</td>
</tr>
<tr>
<td><code>pytest test_shop.py::test_empty_cart_is_zero</code></td>
<td>Run one specific test</td>
</tr>
<tr>
<td><code>pytest --lf</code></td>
<td>Re-run only the tests that failed last time</td>
</tr>
</tbody></table>
<h2>Five habits of good tests</h2>
<ol>
<li><strong>One behaviour per test</strong>, with a name that says what it checks.</li>
<li><strong>Test the edges</strong>: empty lists, zero, the maximum, and invalid input.</li>
<li><strong>Watch every new test fail once</strong>, so you know it can actually catch a bug.</li>
<li><strong>Keep tests fast</strong>, so people run them all the time.</li>
<li><strong>Run them automatically</strong> on every code change, in a CI pipeline.</li>
</ol>
<h2>What&#39;s next</h2>
<p>You have written <strong>unit tests</strong>. In <strong><a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a></strong> we step back to the craft behind every test: <strong>how to write good test cases</strong>, with a template, examples and the techniques testers use to choose them.</p>
<p>When your test suite grows, ProvaLab runs it for you on every change, on real browsers and devices, keeps every result next to its test case, and lets your coding agent read the failures and fix them. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your first Playwright test in 15 minutes (Python)</title>
      <link>https://provalab.io/blog/first-playwright-test/</link>
      <guid isPermaLink="true">https://provalab.io/blog/first-playwright-test/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>ui-testing</category>
      <category>playwright</category>
      <category>pytest</category>
      <description>Install Playwright for Python, write three browser tests against a real website, run them headless, break one on purpose and read the failure, record a trace, and run the suite in CI.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/ui-testing-explained/">Part 1</a> we looked at how UI tests work and why Playwright is the tool to start with. Now we build some. By the end you&#39;ll have <strong>three browser tests running against a real website</strong>, and you&#39;ll know how to read a failure, record a trace of it, and run the suite automatically.</p>
<p>Everything below was run while writing this post, and the output is pasted in unedited.</p>
<p><strong>What you need:</strong> Python 3.10 or newer, a terminal, and about 400 MB of disk for a browser. We&#39;ll test <strong>provalab.io</strong>, our own site. <strong>Only ever run UI tests against sites you own or have permission to test</strong>; point the examples at your own app when you&#39;ve got the hang of it.</p>
<h2>Step 1: Install Playwright (3 minutes)</h2>
<p>Create a folder and a virtual environment, as in <a href="https://provalab.io/blog/first-automated-test-pytest/">Testing Basics Part 5</a>:</p>
<pre><code class="language-bash">mkdir first-ui-test
cd first-ui-test
python3 -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\Activate.ps1
</code></pre>
<p>Install the pytest plugin for Playwright, then the browser it will drive:</p>
<pre><code class="language-bash">pip install pytest-playwright
playwright install chromium
</code></pre>
<p>The second command downloads a copy of Chromium that Playwright controls. It&#39;s separate from the Chrome you browse with, so tests never touch your own cookies or tabs.</p>
<h2>Step 2: Point the tests at your site (1 minute)</h2>
<p>Create <code>pytest.ini</code> so you don&#39;t have to repeat the site address in every test:</p>
<pre><code class="language-ini">[pytest]
addopts = --base-url https://provalab.io
</code></pre>
<p>Now <code>page.goto(&quot;/blog/&quot;)</code> means <code>https://provalab.io/blog/</code>. To test a different site, change this one line.</p>
<h2>Step 3: Write three tests (5 minutes)</h2>
<p>Create <code>test_blog.py</code>:</p>
<pre><code class="language-python">import re

from playwright.sync_api import Page, expect


def test_blog_lists_posts(page: Page):
    page.goto(&quot;/blog/&quot;)
    expect(page.get_by_role(&quot;heading&quot;, level=1)).to_have_text(&quot;ProvaLab Blog&quot;)
    expect(page.get_by_test_id(&quot;blog-card&quot;).first).to_be_visible()


def test_a_post_opens_from_the_list(page: Page):
    page.goto(&quot;/blog/&quot;)
    page.get_by_test_id(&quot;blog-card&quot;).first.get_by_role(&quot;link&quot;).first.click()
    expect(page).to_have_url(re.compile(r&quot;/blog/[a-z0-9-]+/$&quot;))
    expect(page.get_by_role(&quot;heading&quot;, level=1)).to_be_visible()
    expect(page.get_by_role(&quot;link&quot;, name=&quot;All posts&quot;)).to_be_visible()


def test_tour_dialog_opens_and_closes(page: Page):
    page.goto(&quot;/&quot;)
    button = page.locator(&quot;#stage-tms&quot;).get_by_role(&quot;button&quot;, name=&quot;Watch the full tour&quot;)
    button.scroll_into_view_if_needed()
    button.click()
    dialog = page.get_by_role(&quot;dialog&quot;, name=re.compile(&quot;tour&quot;, re.I))
    expect(dialog).to_be_visible()
    page.keyboard.press(&quot;Escape&quot;)
    expect(dialog).to_be_hidden()
</code></pre>
<p>What&#39;s going on:</p>
<ul>
<li><strong><code>page</code></strong> is a fixture the plugin provides: a fresh browser tab for every test, closed afterwards. Tests can&#39;t leak state into each other.</li>
<li><strong>Locate, act, assert</strong>, as in Part 1. <code>get_by_role(&quot;button&quot;, name=&quot;Watch the full tour&quot;)</code> finds the button the way a screen reader would; <code>click()</code> acts; <code>expect(...).to_be_visible()</code> asserts.</li>
<li><strong><code>expect</code> waits for you.</strong> Each assertion retries for up to 5 seconds until the condition is true. That&#39;s why there are no <code>sleep()</code> calls, and why these tests won&#39;t flake when the network is slow.</li>
<li><strong>The third test checks a real interaction:</strong> a button opens a video dialog, and Escape closes it. Checking that something <em>disappears</em> is just as important as checking that it appears.</li>
</ul>
<h2>Step 4: Run them (1 minute)</h2>
<pre><code class="language-bash">pytest -v
</code></pre>
<pre><code class="language-text">collected 3 items

test_blog.py::test_blog_lists_posts[chromium] PASSED                     [ 33%]
test_blog.py::test_a_post_opens_from_the_list[chromium] PASSED           [ 66%]
test_blog.py::test_tour_dialog_opens_and_closes[chromium] PASSED         [100%]

======================== 3 passed, 1 warning in 10.68s =========================
</code></pre>
<p>Three real pages loaded in a real browser, a dialog opened and closed, in under 11 seconds. The browser ran <strong>headless</strong> (no window), which is the default and what CI uses.</p>
<p>(The warning is a deprecation notice inside the plugin itself, not in your code. It&#39;s safe to ignore.)</p>
<h3>Watch it happen</h3>
<p>To see the browser while the test runs, slowed down so you can follow:</p>
<pre><code class="language-bash">pytest --headed --slowmo 500
</code></pre>
<p>Do this whenever a test confuses you. Nine times out of ten you&#39;ll see the problem within seconds: a cookie banner over the button, a page that hasn&#39;t finished loading, a locator pointing at the wrong thing.</p>
<h2>Step 5: Break one on purpose (3 minutes)</h2>
<p>As in Part 5, a test you have never seen fail proves little. Create <code>test_broken.py</code> with a wrong expectation:</p>
<pre><code class="language-python">from playwright.sync_api import Page, expect


def test_blog_heading_wrong_on_purpose(page: Page):
    page.goto(&quot;/blog/&quot;)
    expect(page.get_by_role(&quot;heading&quot;, level=1)).to_have_text(&quot;ProvaLab Articles&quot;)
</code></pre>
<pre><code class="language-bash">pytest test_broken.py
</code></pre>
<pre><code class="language-text">    def test_blog_heading_wrong_on_purpose(page: Page):
        page.goto(&quot;/blog/&quot;)
&gt;       expect(page.get_by_role(&quot;heading&quot;, level=1)).to_have_text(&quot;ProvaLab Articles&quot;)
E       AssertionError: Locator expected to have text &#39;ProvaLab Articles&#39;
E       Actual value: ProvaLab Blog
E       Call log:
E         - Expect &quot;to_have_text&quot; get_by_role(&quot;heading&quot;, level=1) with timeout 5000ms
E         - waiting for get_by_role(&quot;heading&quot;, level=1)
E           14 × locator resolved to &lt;h1 class=&quot;text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900&quot;&gt;ProvaLab Blog&lt;/h1&gt;
E              - unexpected value &quot;ProvaLab Blog&quot;
E
E       Aria snapshot:
E       - heading &quot;ProvaLab Blog&quot; [level=1]

test_broken.py:6: AssertionError
========================= 1 failed, 1 warning in 7.98s =========================
</code></pre>
<p>This is a much richer failure than a plain <code>assert</code>, and learning to read it is the core skill:</p>
<p><img src="https://provalab.io/blog/handson/read-a-failure.svg" alt="An annotated Playwright failure. The AssertionError line restates what the test expected. The Actual value line shows what the page really had. The call log shows the locator resolved to a real h1 fourteen times during the five-second wait, so the element was found and this is a real difference, not a locator or timing problem. The aria snapshot shows the page as a screen reader sees it, useful for choosing a better locator when an element was not found." width="880" height="470" loading="lazy"></p>
<ul>
<li><strong>Expected vs actual</strong> are on adjacent lines. Here the test is wrong, not the site.</li>
<li><strong>&quot;14 × locator resolved to <code>&lt;h1 …&gt;</code>&quot;</strong> says Playwright <em>found</em> the element, checked it 14 times over 5 seconds, and it never had the expected text. So this isn&#39;t a timing or locator problem; it&#39;s a real difference. If the element had <em>not</em> been found, you&#39;d see &quot;waiting for …&quot; with no resolution, and you&#39;d know to fix the locator.</li>
<li><strong>The aria snapshot</strong> shows the page the way a screen reader sees it. When your locator finds nothing, this list tells you what roles and names actually exist, so you can pick the right one.</li>
</ul>
<h2>Step 6: Record a trace (2 minutes)</h2>
<p>A trace is a recording of everything the test did: every action, a screenshot before and after each one, the page&#39;s HTML, console messages and network calls. Keep one for every failure:</p>
<pre><code class="language-bash">pytest test_broken.py --tracing retain-on-failure
</code></pre>
<pre><code class="language-text">1 failed, 1 warning in 6.59s
</code></pre>
<p>The trace lands in <code>test-results/test-broken-py-test-blog-heading-wrong-on-purpose-chromium/trace.zip</code>. Open it:</p>
<pre><code class="language-bash">playwright show-trace test-results/test-broken-py-test-blog-heading-wrong-on-purpose-chromium/trace.zip
</code></pre>
<p>The trace viewer opens in a browser and lets you step through the test, see the page at each action, and inspect what was on screen when it failed. For a failure that only happens in CI, a trace is the difference between fixing it in five minutes and not being able to reproduce it at all.</p>
<p>Two related options, both worth adding to <code>pytest.ini</code>:</p>
<pre><code class="language-ini">[pytest]
addopts = --base-url https://provalab.io --tracing retain-on-failure --screenshot only-on-failure
</code></pre>
<p>(<code>--video retain-on-failure</code> records a video too, at the cost of slower runs.)</p>
<h2>Step 7: Run it in the pipeline (3 minutes)</h2>
<p>The same tests, run on every change by GitHub Actions, with traces saved when anything fails:</p>
<pre><code class="language-yaml">name: ui-tests
on: [push, pull_request]

jobs:
  ui:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: &quot;3.12&quot;
      - run: pip install pytest-playwright
      - run: playwright install --with-deps chromium
      - run: pytest --tracing retain-on-failure
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-traces
          path: test-results/
</code></pre>
<p><code>--with-deps</code> installs the system libraries the browser needs on a fresh CI machine. The last step uploads the traces as a downloadable artifact whenever the run is red.</p>
<p><img src="https://provalab.io/blog/handson/run-it-three-places.svg" alt="One test file, three places to run it. On your laptop while writing: headed and slow motion, seconds. In the CI pipeline on every change: headless with traces kept on failure, minutes, blocks the merge if red. On a browser and device grid before a release: many browsers, operating systems and real phones in parallel, every run recorded. The test code is identical everywhere." width="880" height="400" loading="lazy"></p>
<p>When you need more than one machine can give you, many browsers, operating systems and real phones at once, the same tests connect to a <strong>browser grid</strong> instead of a local Chromium. The test code doesn&#39;t change.</p>
<h2>Handy options</h2>
<table>
<thead>
<tr>
<th>Command</th>
<th>What it does</th>
</tr>
</thead>
<tbody><tr>
<td><code>pytest --headed</code></td>
<td>Show the browser window</td>
</tr>
<tr>
<td><code>pytest --slowmo 500</code></td>
<td>Pause 500 ms between actions, so you can watch</td>
</tr>
<tr>
<td><code>pytest --browser firefox</code></td>
<td>Run in Firefox (after <code>playwright install firefox</code>); also <code>webkit</code> for Safari&#39;s engine</td>
</tr>
<tr>
<td><code>pytest --browser chromium --browser firefox</code></td>
<td>Run every test in both</td>
</tr>
<tr>
<td><code>pytest --tracing on</code></td>
<td>Record a trace for every test, not only failures</td>
</tr>
<tr>
<td><code>pytest -k tour</code></td>
<td>Run only tests with &quot;tour&quot; in the name</td>
</tr>
<tr>
<td><code>playwright codegen https://provalab.io</code></td>
<td>Click around a site and have Playwright write the locators for you</td>
</tr>
</tbody></table>
<p><code>codegen</code> is a good way to learn the locator syntax: it records your clicks and suggests role-based locators, which you can then tidy up.</p>
<h2>Habits worth keeping</h2>
<ol>
<li><strong>Prefer <code>get_by_role</code> and <code>get_by_test_id</code>.</strong> The <code>#stage-tms</code> CSS selector in the third test is a reasonable exception: it&#39;s an ID set on purpose, and it narrows the search to one section.</li>
<li><strong>Never <code>time.sleep()</code>.</strong> If you feel the need, you&#39;re missing an <code>expect</code>.</li>
<li><strong>Each test starts from <code>page.goto()</code></strong> and sets up what it needs. No test should depend on another.</li>
<li><strong>Keep traces on failure, always.</strong> They cost nothing when tests pass.</li>
<li><strong>Run headed when confused</strong>, headless everywhere else.</li>
</ol>
<h2>What&#39;s next</h2>
<p>Our three tests all go through the user interface. In <strong><a href="https://provalab.io/blog/api-testing-explained/">Part 3</a></strong> we go one layer down: <strong>API testing</strong>, where the same checks run in milliseconds instead of seconds, and why many teams test the API before the UI.</p>
<p>ProvaLab&#39;s QA Infrastructure is the &quot;third place&quot; in the diagram above: your Playwright and Selenium tests run on real browsers and devices in the cloud, every session is recorded, and when a test fails the recording and logs go straight back to your coding agent. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your first security scan with OWASP ZAP (hands-on)</title>
      <link>https://provalab.io/blog/first-security-scan-owasp-zap/</link>
      <guid isPermaLink="true">https://provalab.io/blog/first-security-scan-owasp-zap/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>security-testing</category>
      <category>owasp-zap</category>
      <category>dast</category>
      <description>Run a real DAST scan with OWASP ZAP against the API from Part 4, read the report, plant a SQL injection and watch the scanner find it, triage the findings, fix the bug and re-scan.</description>
      <content:encoded><![CDATA[<p><a href="https://provalab.io/blog/security-testing-basics-owasp-top-10/">Part 5</a> explained the OWASP Top 10 and the six kinds of security testing. This post does one of them for real: a <strong>DAST scan</strong>, where a tool attacks a running application from the outside, the way an attacker would.</p>
<p>We&#39;ll use <strong>OWASP ZAP</strong>, the most widely used free security scanner, against the discount API we built in <a href="https://provalab.io/blog/first-api-test-pytest/">Part 4</a>. Then we&#39;ll plant a real vulnerability and watch the scanner find it.</p>
<p>Every command and every line of output below was run while writing this post.</p>
<p><strong>Before you start:</strong> a DAST scan sends thousands of hostile requests. <strong>Only ever point it at something you own</strong>, and never at a production system: it will create records, trip rate limits and fill logs. Our target runs on our own laptop.</p>
<h2>How a DAST scan works</h2>
<p><img src="https://provalab.io/blog/handson/dast-how-it-works.svg" alt="Four stages. Discover: the scanner learns the URLs by crawling or importing an OpenAPI document. Passive scan: it inspects normal responses for missing security headers and leaked information without sending anything hostile. Active scan: it sends attack payloads to every input and watches the reaction; test environments only. Report: findings grouped by risk, each with the request that triggered it, the evidence and a fix." width="880" height="420" loading="lazy"></p>
<p>The scanner has to <strong>discover</strong> the URLs first. For a website it crawls the pages; for an API it reads the <strong>OpenAPI document</strong>, which lists every endpoint and parameter. FastAPI generates one for us automatically at <code>/openapi.json</code>, which is why the Part 4 API is a convenient target.</p>
<h2>Step 1: Get ZAP (2 minutes, plus a download)</h2>
<p>ZAP ships as a container image, so there&#39;s nothing to install beyond Docker:</p>
<pre><code class="language-bash">docker pull ghcr.io/zaproxy/zaproxy:stable
</code></pre>
<p>It&#39;s large (about 3.7 GB), because it carries a full Java runtime and a browser.</p>
<h2>Step 2: Start the target (1 minute)</h2>
<p>In the <code>first-api-test</code> folder from Part 4, start the API and leave it running:</p>
<pre><code class="language-bash">uvicorn app:app
</code></pre>
<h2>Step 3: Run the scan (5 minutes)</h2>
<p>ZAP includes a ready-made <strong>API scan</strong> script. One command runs discovery, the passive scan and the active scan, and writes a report:</p>
<pre><code class="language-bash">mkdir -p zap &amp;&amp; chmod 777 zap
docker run --rm --network host -v &quot;$PWD/zap:/zap/wrk:rw&quot; ghcr.io/zaproxy/zaproxy:stable \
  zap-api-scan.py -t http://127.0.0.1:8000/openapi.json -f openapi \
  -J report.json -w report.md -I \
  -z &quot;-config replacer.full_list(0).description=auth \
      -config replacer.full_list(0).enabled=true \
      -config replacer.full_list(0).matchtype=REQ_HEADER \
      -config replacer.full_list(0).matchstr=Authorization \
      -config replacer.full_list(0).regex=false \
      -config replacer.full_list(0).replacement=Bearer\ token-alice&quot;
</code></pre>
<p>What the pieces mean:</p>
<ul>
<li><code>--network host</code> lets the container reach the API on your machine. (On macOS and Windows, use <code>http://host.docker.internal:8000</code> instead of <code>127.0.0.1</code> and drop <code>--network host</code>.)</li>
<li><code>-t …/openapi.json -f openapi</code> tells ZAP to import the endpoints from the OpenAPI document.</li>
<li><code>-J</code> and <code>-w</code> write JSON and Markdown reports into the <code>zap</code> folder.</li>
<li><code>-I</code> stops the script from returning a failure code on warnings, so it finishes cleanly.</li>
<li>The long <code>-z</code> part configures ZAP&#39;s <strong>replacer</strong> to add <code>Authorization: Bearer token-alice</code> to every request. Without it, every endpoint would answer <code>401</code> and the scanner would test nothing but the sign-in wall.</li>
<li>If the output ends with &quot;Failed to start the main proxy: Address already in use&quot;, something on your machine is using port 8080, which ZAP wants for itself. Add <code>-P 8099</code> (any free port) after <code>zap-api-scan.py</code>. This happened to us on the third run.</li>
</ul>
<p>Three minutes later, the end of the output:</p>
<pre><code class="language-text">Number of Imported URLs: 6
Total of 35 URLs
PASS: Directory Browsing [0]
PASS: Vulnerable JS Library (Powered by Retire.js) [10003]
...
PASS: Remote OS Command Injection (Time Based) [90037]
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 1
	http://127.0.0.1:8000/openapi.json (200 OK)
WARN-NEW: Cross-Origin-Resource-Policy Header Missing or Invalid [90004] x 1
	http://127.0.0.1:8000/openapi.json (200 OK)
FAIL-NEW: 0	FAIL-INPROG: 0	WARN-NEW: 2	WARN-INPROG: 0	INFO: 0	IGNORE: 0	PASS: 118
</code></pre>
<p><strong>118 checks passed, 2 warnings, no failures.</strong> Both warnings are the same kind of thing: a recommended security header the server doesn&#39;t send, flagged on the OpenAPI document itself. ZAP rates them <strong>Low</strong>. For a tiny API with no browser pages they barely matter, but they&#39;re real and the fix is one line of middleware, so they go on the backlog.</p>
<p>(A note on the words: the API-scan script reports every finding as <code>WARN</code> unless you give it a rules file saying which ones should <code>FAIL</code> the build. The actual risk level, High to Informational, is in the report.)</p>
<h2>What the scanner cannot see</h2>
<p>Remember the most valuable test from Part 4: Alice asking for Bob&#39;s order and getting <code>403</code>. <strong>ZAP can&#39;t find that bug.</strong> It has one token, Alice&#39;s, and no way of knowing that order 43 isn&#39;t hers. If the API wrongly returned Bob&#39;s order, the scanner would see a perfectly healthy <code>200</code>.</p>
<p>This is the single most important thing to understand about security scanners: <strong>they find the generic mistakes, not the ones specific to your business rules.</strong> The number-one risk in the OWASP Top 10, broken access control, is mostly found by tests <em>you</em> write. The scanner and your test suite are partners, not alternatives.</p>
<h2>Step 4: Plant a vulnerability (3 minutes)</h2>
<p>To see what a real finding looks like, let&#39;s add two things a team might ship by accident. Append this to <code>app.py</code>:</p>
<pre><code class="language-python"># ---- two things a real team might ship by accident ----
import sqlite3
import traceback

from fastapi import Request
from fastapi.responses import PlainTextResponse

db = sqlite3.connect(&quot;:memory:&quot;, check_same_thread=False)
db.execute(&quot;create table restaurants(name text, cuisine text)&quot;)
db.executemany(&quot;insert into restaurants values (?, ?)&quot;,
               [(&quot;Pizza Palace&quot;, &quot;pizza&quot;), (&quot;Curry House&quot;, &quot;indian&quot;), (&quot;Sushi Go&quot;, &quot;japanese&quot;)])


@app.get(&quot;/restaurants&quot;)
def search_restaurants(q: str = &quot;&quot;, customer: str = Depends(current_customer)):
    # the query is built by gluing the user&#39;s text into the SQL
    rows = db.execute(f&quot;select name, cuisine from restaurants where name like &#39;%{q}%&#39;&quot;).fetchall()
    return [{&quot;name&quot;: n, &quot;cuisine&quot;: c} for n, c in rows]


@app.exception_handler(Exception)
async def show_errors(request: Request, exc: Exception):
    # &quot;debug mode&quot;: send the full stack trace back to whoever asked
    return PlainTextResponse(&quot;&quot;.join(traceback.format_exception(exc)), status_code=500)
</code></pre>
<p>The first is <strong>A05 Injection</strong>: the search text is glued straight into an SQL statement. The second is <strong>A10 Mishandling of Exceptional Conditions</strong> (and a bit of A02): any crash sends the full stack trace to the caller.</p>
<p>Restart <code>uvicorn</code>, and try the search with a single quote, the classic probe:</p>
<pre><code class="language-bash">curl &quot;http://127.0.0.1:8000/restaurants?q=%27&quot; -H &quot;Authorization: Bearer token-alice&quot;
</code></pre>
<pre><code class="language-text">Traceback (most recent call last):
  ...
sqlite3.OperationalError: unrecognized token: &quot;&#39;&quot;
</code></pre>
<p>One character broke the query, and the server told us exactly which database it uses and what went wrong. That&#39;s what an attacker&#39;s first probe looks like, and what their first clue looks like.</p>
<h2>Step 5: Scan again</h2>
<p>Run the same <code>docker run</code> command. This time:</p>
<pre><code class="language-text">WARN-NEW: A Server Error response code was returned by the server [100000] x 1
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 2
WARN-NEW: SQL Injection [40018] x 1
WARN-NEW: Cross-Origin-Resource-Policy Header Missing or Invalid [90004] x 2
FAIL-NEW: 0	FAIL-INPROG: 0	WARN-NEW: 4	WARN-INPROG: 0	INFO: 0	IGNORE: 0	PASS: 117
</code></pre>
<p>ZAP found the injection. The detail is in the report:</p>
<pre><code class="language-text">[High (Low)] SQL Injection (plugin 40018) x1  CWE 89
   uri:      http://127.0.0.1:8000/restaurants?q=%27 | method: GET | param: q
   attack:   &#39;
   evidence: HTTP/1.1 500 Internal Server Error
   solution: Do not trust client side input, even if there is client side validation
             in place. In general, type check all data on the server side. ...
             use PreparedStatement ... with parameters passed by &#39;?&#39;

[Low (High)] A Server Error response code was returned by the server (plugin 100000) x1
   uri:      http://127.0.0.1:8000/restaurants?q=5%3BURL%3D... | method: GET
   evidence: 500
</code></pre>
<p>How to read a finding:</p>
<ul>
<li><strong><code>High (Low)</code></strong> means <em>High risk, Low confidence</em>. ZAP saw a <code>500</code> when it sent a quote, which is a strong hint but not proof. <strong>Confirm every finding by hand before you act on it.</strong> We already did: the curl above reproduced it.</li>
<li><strong><code>param: q</code>, <code>attack: &#39;</code></strong> tell you exactly where and how. That&#39;s your bug report&#39;s steps to reproduce.</li>
<li><strong>CWE 89</strong> is the standard identifier for SQL injection, useful for looking up fixes and for compliance reports.</li>
<li>The server-error finding is the <em>symptom</em> of the same bug plus the debug handler: rated Low on its own, but the stack trace it leaks is a gift to an attacker.</li>
</ul>
<h2>Step 6: Triage</h2>
<p>A scan report is not a to-do list. Sort it:</p>
<p><img src="https://provalab.io/blog/handson/triage-findings.svg" alt="The findings sorted into three piles. Fix now: SQL Injection on GET /restaurants?q= (High), reproduced by hand with a single quote, raise a bug and fix today with a parameterised query; and the 500 with a stack trace, same root cause plus debug mode. Backlog, real but minor: X-Content-Type-Options and Cross-Origin-Resource-Policy headers missing (Low), one line of server config each. Not applicable: 22 informational notes that the API returned 404, and 4 about non-storable content, both expected for an API; mark them so the next scan is quieter." width="880" height="440" loading="lazy"></p>
<ul>
<li><strong>Fix now:</strong> the injection and the stack-trace leak. Confirmed, serious, same root cause.</li>
<li><strong>Backlog:</strong> the two headers. Real, minor, one line each.</li>
<li><strong>Not applicable:</strong> the 22 informational notes that the API returned <code>404</code> for things that don&#39;t exist, and the 4 about responses not being cacheable. An API is <em>supposed</em> to do both. Mark them as not applicable in your tracker so the next scan doesn&#39;t repeat them.</li>
</ul>
<p>Thirty lines of report become <strong>two bugs and two backlog items</strong>. The <a href="https://provalab.io/blog/how-to-write-a-bug-report/">bug report</a> for the injection writes itself: the URL, the parameter, the payload <code>&#39;</code>, the expected result (a normal empty list or a <code>400</code>), the actual result (a <code>500</code> with a stack trace), and the CWE.</p>
<h2>Step 7: Fix and re-scan</h2>
<p>Two changes. The query takes a parameter instead of glued text, and the debug handler goes:</p>
<pre><code class="language-python">    rows = db.execute(&quot;select name, cuisine from restaurants where name like ?&quot;, (f&quot;%{q}%&quot;,)).fetchall()
</code></pre>
<p>Restart, and the single quote now returns an empty list with a <code>200</code>, as it should:</p>
<pre><code class="language-bash">curl &quot;http://127.0.0.1:8000/restaurants?q=%27&quot; -H &quot;Authorization: Bearer token-alice&quot;
</code></pre>
<pre><code class="language-text">[]
</code></pre>
<p>Scan a third time:</p>
<pre><code class="language-text">Number of Imported URLs: 7
Total of 36 URLs
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 2
WARN-NEW: Cross-Origin-Resource-Policy Header Missing or Invalid [90004] x 2
FAIL-NEW: 0	FAIL-INPROG: 0	WARN-NEW: 2	WARN-INPROG: 0	INFO: 0	IGNORE: 0	PASS: 118
</code></pre>
<p>The injection and the server error are gone; 118 checks pass again.</p>
<p>The two header warnings are still there, as expected; they&#39;re on the backlog, and a scanner will keep mentioning them until the header is added. That&#39;s the right behaviour: a scan is a regression test for security.</p>
<h2>Putting it in the pipeline</h2>
<p>The same command runs in CI. Start the API, run the scan against it, and keep the report as an artifact:</p>
<pre><code class="language-yaml">      - run: pip install -r requirements.txt
      - run: uvicorn app:app &amp;
      - run: mkdir -p zap &amp;&amp; chmod 777 zap
      - run: &gt;
          docker run --rm --network host -v &quot;$PWD/zap:/zap/wrk:rw&quot;
          ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py
          -t http://127.0.0.1:8000/openapi.json -f openapi -J report.json -w report.md
          -z &quot;-config replacer.full_list(0).description=auth -config replacer.full_list(0).enabled=true -config replacer.full_list(0).matchtype=REQ_HEADER -config replacer.full_list(0).matchstr=Authorization -config replacer.full_list(0).regex=false -config replacer.full_list(0).replacement=Bearer\ ${{ secrets.TEST_API_TOKEN }}&quot;
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-report
          path: zap/
</code></pre>
<p>Without <code>-I</code>, the job fails when ZAP finds anything new, which is what you want once the backlog items are either fixed or listed in a rules file as ignored. Run it nightly rather than on every commit: a full active scan takes minutes, not seconds.</p>
<h2>Habits worth keeping</h2>
<ol>
<li><strong>Scan with a signed-in session.</strong> An unauthenticated scan tests your sign-in page and nothing else.</li>
<li><strong>Confirm by hand before raising a bug.</strong> &quot;High (Low)&quot; is a lead, not a verdict.</li>
<li><strong>Triage into fix-now, backlog, not-applicable</strong>, and record the last pile so it stops reappearing.</li>
<li><strong>Keep writing access-control tests yourself.</strong> The scanner can&#39;t.</li>
<li><strong>Never scan what you don&#39;t own.</strong></li>
</ol>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/first-load-test-k6/">Part 7</a></strong>, the last of this series, we go hands-on with <strong>load testing</strong>: a k6 script against the same API, finding where it slows down and where it breaks.</p>
<p>ProvaLab AppSec runs this kind of scan, and API fuzzing, dependency, secrets, container and code scans, against targets you&#39;ve verified you own, from the cloud, and presents the findings as a plain-language fix list ordered by severity, with your confirmed false positives remembered between runs. See it in the <a href="https://provalab.io/#stage-appsec">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How do you test code your AI coding agent wrote?</title>
      <link>https://provalab.io/blog/how-to-test-code-your-ai-agent-wrote/</link>
      <guid isPermaLink="true">https://provalab.io/blog/how-to-test-code-your-ai-agent-wrote/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing</category>
      <category>ai-agents</category>
      <description>AI agents write features in minutes, but most teams still check that code the old way. Here is a practical way to verify AI-written code before it ships.</description>
      <content:encoded><![CDATA[<p>Claude Code, Cursor and Copilot can now write a whole feature while you get a coffee. That part of the job got fast. The part after it, checking that the code actually works, did not.</p>
<p>Most developers have already made the switch. In the <a href="https://survey.stackoverflow.co/2025/ai">Stack Overflow Developer Survey 2025</a>, 84% of developers said they use or plan to use AI tools in their work. But trust went the other way: 46% said they actively distrust the accuracy of what those tools produce.</p>
<p>So teams end up in an awkward place. They write code faster than ever, then either slow down to check it by hand, or ship it and hope.</p>
<h2>Why AI-written code needs a different kind of checking</h2>
<p>AI-written code is not worse than human code by default. It fails in different ways:</p>
<ul>
<li><strong>It looks right.</strong> The code is tidy and confident, so reviewers skim it.</li>
<li><strong>It passes its own tests.</strong> If the agent wrote the code and the tests together, the tests often check what the code does, not what it should do.</li>
<li><strong>It handles the happy path.</strong> Empty lists, slow networks, expired sessions and two users at once are where it tends to break.</li>
<li><strong>There is a lot of it.</strong> A reviewer who could read 200 lines a day now gets 2,000.</li>
</ul>
<p>The answer is not to read every line more carefully. It is to make checking as fast as writing.</p>
<h2>A simple routine for every AI-written change</h2>
<h3>1. Write the test cases before the code</h3>
<p>Ask the agent for the test cases first, in plain language: what the feature must do, and what must never happen. Read that list yourself. It is ten lines instead of five hundred, and it is where your judgement matters most.</p>
<h3>2. Run the tests somewhere real</h3>
<p>Tests that only pass on the developer&#39;s laptop prove little. Run them on real browsers and devices, against a running build, the way a user would meet the feature.</p>
<h3>3. Keep a record of every run</h3>
<p>When a test fails next week, you want to know whether it ever passed, on which build, and what changed. A test case and its run history should live in one place, not in a chat log.</p>
<h3>4. Check what tests don&#39;t cover</h3>
<p>Functional tests don&#39;t tell you whether an API leaks data or falls over when fifty people use it at once. Add a security scan and a short load test before anything important ships.</p>
<h3>5. Send failures back to the agent</h3>
<p>This is the step most teams skip. When a test fails, the agent that wrote the code should get the failure, the logs and a recording, and fix it itself. A person approves the result instead of doing the triage.</p>
<h2>Where ProvaLab fits</h2>
<p>We built ProvaLab so a coding agent can do this whole routine on its own. Through the Claude Code plugin or MCP, the agent runs your tests on cloud browsers and devices, records every result in test management, scans for security issues, runs load tests, and gets the failures back so it can fix them.</p>
<p>You can see it working in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>, or <a href="https://provalab.io/demo">book a 30-minute demo</a> and we will run it against your own app.</p>
<p>AI made writing code fast. Checking it should be just as fast.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to write a good bug report (with template and examples)</title>
      <link>https://provalab.io/blog/how-to-write-a-bug-report/</link>
      <guid isPermaLink="true">https://provalab.io/blog/how-to-write-a-bug-report/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>bug-report</category>
      <description>What makes a bug report easy to act on, every field to include, severity vs priority explained, a good and a bad example side by side, and a copy-ready template.</description>
      <content:encoded><![CDATA[<p>Finding a bug is only half the job. If the developer can&#39;t understand it or reproduce it, the bug doesn&#39;t get fixed. It gets bounced back as &quot;works on my machine&quot;, and everyone loses a day.</p>
<p>A good bug report is one of the most valuable things a tester produces. This part shows you how to write one.</p>
<h2>What is a bug report?</h2>
<p><strong>A bug report (also called a defect report or an issue) is a written record of a problem: what went wrong, how to make it happen again, and why it matters.</strong></p>
<p>Its job is simple: <strong>let someone who has never seen the bug reproduce it, understand it and fix it, without asking you a single question.</strong></p>
<h2>Same bug, two reports</h2>
<p><img src="https://provalab.io/blog/basics/bug-report-good-bad.svg" alt="The same bug reported two ways. The bad report says only &quot;Discount not working. I tried a code and it doesn't work. Please fix ASAP!!&quot; The good report BUG-231 is titled &quot;Expired discount code accepted at checkout; total reduced by 10%&quot;, and gives the environment, three steps to reproduce, expected and actual results, severity, priority, reproducibility 5 out of 5, attachments and links to the test case and requirement." width="880" height="560" loading="lazy"></p>
<p>The bad report starts a conversation: which code, which app, what happened? The good one ends it.</p>
<h2>The fields of a good bug report</h2>
<table>
<thead>
<tr>
<th>Field</th>
<th>What to write</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ID</strong></td>
<td>Usually assigned by your tracker</td>
<td>BUG-231</td>
</tr>
<tr>
<td><strong>Title</strong></td>
<td>Where, what goes wrong, and when</td>
<td>Expired discount code accepted at checkout; total reduced by 10%</td>
</tr>
<tr>
<td><strong>Environment</strong></td>
<td>App version, browser or device, OS, server</td>
<td>Web 2.4.0, Chrome 131, Windows 11, staging</td>
</tr>
<tr>
<td><strong>Preconditions</strong></td>
<td>What must be true first</td>
<td>Signed in as test-user-2; code OLD10 expired on 1 Nov</td>
</tr>
<tr>
<td><strong>Steps to reproduce</strong></td>
<td>Short, numbered, the fewest needed</td>
<td>1. Add items worth 500 · 2. Open checkout · 3. Apply OLD10</td>
</tr>
<tr>
<td><strong>Expected result</strong></td>
<td>What should happen</td>
<td>&quot;This code has expired&quot;; total stays 500</td>
</tr>
<tr>
<td><strong>Actual result</strong></td>
<td>What really happens</td>
<td>Code accepted; total becomes 450</td>
</tr>
<tr>
<td><strong>Severity</strong></td>
<td>How badly it breaks the software</td>
<td>High</td>
</tr>
<tr>
<td><strong>Priority</strong></td>
<td>How soon it should be fixed</td>
<td>High</td>
</tr>
<tr>
<td><strong>Reproducibility</strong></td>
<td>How often it happens</td>
<td>5 out of 5 tries</td>
</tr>
<tr>
<td><strong>Attachments</strong></td>
<td>Proof and clues</td>
<td>Screenshot, screen recording, console log, network log</td>
</tr>
<tr>
<td><strong>Links</strong></td>
<td>Where it came from</td>
<td>Test case TC-104, requirement REQ-12</td>
</tr>
</tbody></table>
<h3>Writing a good title</h3>
<p>Most people only ever read the title, in a list of fifty bugs. Use this pattern:</p>
<blockquote>
<p><strong>[Where] + [what goes wrong] + [under what condition]</strong></p>
</blockquote>
<ul>
<li>Bad: &quot;Checkout bug&quot;</li>
<li>Bad: &quot;Discount doesn&#39;t work properly&quot;</li>
<li>Good: &quot;Expired discount code accepted at checkout; total reduced by 10%&quot;</li>
<li>Good: &quot;Order history page blank on Android 14 after switching language&quot;</li>
</ul>
<h3>Expected vs actual: the heart of the report</h3>
<p>Keep these two <strong>side by side and specific</strong>. &quot;It doesn&#39;t work&quot; is not an actual result. &quot;The total becomes 450 and no error appears&quot; is. If the expected behaviour comes from a requirement or test case, link to it: that shows it&#39;s a bug and not a matter of opinion.</p>
<h2>Severity vs priority</h2>
<p>These two get confused constantly, and they are <strong>not the same thing</strong>:</p>
<ul>
<li><strong>Severity</strong> is about the <strong>software</strong>: how badly does the bug break it? Usually set by the tester.</li>
<li><strong>Priority</strong> is about the <strong>business</strong>: how soon must it be fixed? Usually set by the product owner or team lead.</li>
</ul>
<p>They often match, but not always:</p>
<p><img src="https://provalab.io/blog/basics/severity-priority.svg" alt="Severity versus priority. High severity, high priority: payment fails for every card. High severity, low priority: the app crashes exporting orders older than ten years, which almost nobody does. Low severity, high priority: the brand name is misspelled on the home page. Low severity, low priority: a tooltip slightly misaligned on one old browser." width="880" height="460" loading="lazy"></p>
<p>Common severity levels:</p>
<table>
<thead>
<tr>
<th>Severity</th>
<th>Meaning</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Critical</strong></td>
<td>System down, data lost, security hole, no workaround</td>
<td>Payment fails for everyone; other users&#39; orders visible</td>
</tr>
<tr>
<td><strong>High</strong></td>
<td>A major feature is broken</td>
<td>Expired codes are accepted</td>
</tr>
<tr>
<td><strong>Medium</strong></td>
<td>A feature misbehaves, but there is a workaround</td>
<td>Discount only applies after refreshing the page</td>
</tr>
<tr>
<td><strong>Low</strong></td>
<td>Cosmetic or minor</td>
<td>Button text slightly misaligned</td>
</tr>
</tbody></table>
<h2>Attachments that save hours</h2>
<ul>
<li><strong>Screenshot</strong>, with the problem area highlighted.</li>
<li><strong>Screen recording</strong> for anything involving several steps or timing. A short video often replaces a page of text.</li>
<li><strong>Console log</strong> from the browser&#39;s developer tools (F12 → Console) for web bugs.</li>
<li><strong>Network log or HAR file</strong> when a server request fails. It shows exactly what was sent and what came back.</li>
<li><strong>Server or app logs</strong>, with the time the bug happened.</li>
<li><strong>Test data:</strong> the account, order number or file you used.</li>
</ul>
<p><strong>Never attach real passwords, card numbers or customers&#39; personal data.</strong> Use test accounts, and blur anything sensitive in screenshots.</p>
<h2>Ten habits of great bug reporters</h2>
<ol>
<li><strong>Reproduce it first.</strong> Try at least twice before reporting.</li>
<li><strong>Search for duplicates.</strong> Someone may already have reported it.</li>
<li><strong>One bug per report.</strong> Two problems means two reports, because they may be fixed by different people at different times.</li>
<li><strong>Use the fewest steps</strong> that still trigger the bug.</li>
<li><strong>Find the boundaries.</strong> Does it happen in other browsers? With other codes? Only for new customers? Each answer narrows the cause.</li>
<li><strong>Report facts, not guesses.</strong> &quot;The total becomes 450&quot;, not &quot;the discount logic is broken&quot;.</li>
<li><strong>Keep the tone neutral.</strong> No blame, no capitals, no &quot;!!!&quot;. Developers and testers are on the same side.</li>
<li><strong>Write for a stranger.</strong> Assume the reader has never seen this part of the app.</li>
<li><strong>Say if it&#39;s intermittent.</strong> &quot;Happens about 2 in 5 tries&quot; is useful; hiding it wastes time.</li>
<li><strong>Keep it updated.</strong> If you learn more, add it to the report, not to a chat message that will be lost.</li>
</ol>
<h2>A copy-ready template</h2>
<pre><code class="language-text">Title: [Where] [what goes wrong] [under what condition]

Environment: app version, browser/device, OS, server (staging/production)
Preconditions: account, data or settings needed first

Steps to reproduce:
1.
2.
3.

Expected result:
Actual result:

Severity: Critical / High / Medium / Low
Priority: High / Medium / Low
Reproducibility: e.g. 5/5, or 2 in 5 tries

Attachments: screenshot, recording, console log, network log
Links: test case, requirement, related bugs
</code></pre>
<h2>What&#39;s next</h2>
<p>Once a bug is reported, its journey has only just begun. In <strong><a href="https://provalab.io/blog/bug-life-cycle/">Part 10</a></strong> we follow it through the <strong>bug life cycle</strong>: new, assigned, fixed, retested and closed (or reopened).</p>
<p>In ProvaLab, a failed automated test can raise a bug report with the steps, the error, the screen recording and the logs already attached, linked to its test case and test run. Your coding agent can read it and fix the code. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to write a test plan (with a one-page example)</title>
      <link>https://provalab.io/blog/how-to-write-a-test-plan/</link>
      <guid isPermaLink="true">https://provalab.io/blog/how-to-write-a-test-plan/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-plan</category>
      <description>What a test plan is, the sections every plan needs, a one-page example you can copy, risk-based testing, and how test plans work in agile teams.</description>
      <content:encoded><![CDATA[<p>So far you have learned how to <a href="https://provalab.io/blog/how-to-write-test-cases/">write test cases</a> and how to <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">choose them with test design techniques</a>. But before anyone runs a single test, the team needs to agree on a bigger picture: <strong>what are we testing this time, how, who does it, by when, and how do we know we&#39;re done?</strong></p>
<p>That agreement is the <strong>test plan</strong>. In the <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">testing life cycle</a> it is Phase 2, and everything after it follows from it.</p>
<h2>What is a test plan?</h2>
<p><strong>A test plan is a document that describes the scope, approach, resources and schedule of the testing for a release or project.</strong></p>
<p>It answers six questions:</p>
<ol>
<li><strong>Why</strong> are we testing? (the objective)</li>
<li><strong>What</strong> will we test, and what won&#39;t we? (scope)</li>
<li><strong>How</strong> will we test it? (approach)</li>
<li><strong>Where</strong> will the tests run? (environment)</li>
<li><strong>Who</strong> does what, and <strong>when</strong>? (people and schedule)</li>
<li><strong>When</strong> do we start, and when are we done? (entry and exit criteria)</li>
</ol>
<p>A good test plan is written for <strong>people</strong>, not for a filing cabinet. Developers, product managers and testers should all be able to read it in a few minutes and agree.</p>
<h2>Test plan vs test strategy</h2>
<ul>
<li>A <strong>test strategy</strong> is the long-term, organisation-wide approach: &quot;We automate all regression tests, every release gets a security scan, and we test on the three most-used browsers.&quot; It rarely changes.</li>
<li>A <strong>test plan</strong> applies that strategy to <strong>one release or project</strong>: &quot;For release 2.4 we test discount codes on web and Android from 3 to 7 November.&quot;</li>
</ul>
<p>Small teams often have no separate strategy document; its key rules simply live inside each plan.</p>
<h2>The sections of a test plan</h2>
<p>Formal templates exist. The IEEE 829 standard was the classic one, and it was later replaced by ISO/IEC/IEEE 29119-3. But most teams use a lighter version of the same ideas:</p>
<table>
<thead>
<tr>
<th>Section</th>
<th>What it says</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Objective</strong></td>
<td>What this testing must prove</td>
<td>Customers can apply valid discount codes and are refused invalid ones</td>
</tr>
<tr>
<td><strong>Scope: in</strong></td>
<td>Features and platforms you will test</td>
<td>Discount codes on web and Android; cart, checkout, order history</td>
</tr>
<tr>
<td><strong>Scope: out</strong></td>
<td>What you deliberately won&#39;t test, and why</td>
<td>iPhone app (next release); restaurant dashboard (unchanged)</td>
</tr>
<tr>
<td><strong>Approach</strong></td>
<td>Test levels, types, and manual vs automated</td>
<td>Functional, API and regression tests; regression automated</td>
</tr>
<tr>
<td><strong>Environment</strong></td>
<td>Where tests run, with what data</td>
<td>Staging server, test codes, 3 test accounts, payment sandbox</td>
</tr>
<tr>
<td><strong>Entry criteria</strong></td>
<td>When testing can start</td>
<td>Build deployed to staging; smoke test passed</td>
</tr>
<tr>
<td><strong>Exit criteria</strong></td>
<td>When testing is done</td>
<td>All high-priority tests passed; no open critical bugs</td>
</tr>
<tr>
<td><strong>Suspension criteria</strong></td>
<td>When to pause testing</td>
<td>Pause if sign-in is broken, because most tests depend on it</td>
</tr>
<tr>
<td><strong>People</strong></td>
<td>Who does what</td>
<td>One test lead, two testers; a developer on call for blockers</td>
</tr>
<tr>
<td><strong>Schedule</strong></td>
<td>Key dates</td>
<td>Testing 3–7 November; summary report 8 November</td>
</tr>
<tr>
<td><strong>Deliverables</strong></td>
<td>What testing produces</td>
<td>Test cases, run results, bug reports, a summary report</td>
</tr>
<tr>
<td><strong>Risks</strong></td>
<td>What could go wrong, and the backup plan</td>
<td>Payment sandbox may be slow: book it early, keep a mock ready</td>
</tr>
</tbody></table>
<p><strong>&quot;Out of scope&quot; is the most underrated line in any test plan.</strong> Writing down what you <em>won&#39;t</em> test avoids the worst surprise of all: a bug in an area everyone thought someone else had checked.</p>
<h2>A one-page example</h2>
<p>For most releases, the whole plan fits on one page:</p>
<p><img src="https://provalab.io/blog/basics/test-plan-one-page.svg" alt="A one-page test plan for release 2.4, discount codes. Objective: customers can apply valid codes and are clearly refused invalid ones. In scope: discount codes on web and Android, cart, checkout and order history. Out of scope: iPhone app and restaurant dashboard. Approach: functional, API and regression tests, regression automated. Environment: staging with test codes, 3 test accounts and a card sandbox. Entry: build on staging, smoke passed. Exit: high-priority tests passed, no critical bugs. People: 1 test lead, 2 testers. Schedule: 3–7 November, report 8 November. Risk: payment sandbox may be slow; book it early and keep a mock ready." width="880" height="560" loading="lazy"></p>
<p>If your plan doesn&#39;t fit on one or two pages, ask whether every section is really helping someone make a decision.</p>
<h2>Risk-based testing: deciding what matters most</h2>
<p>There is never time to test everything equally. <strong>Risk-based testing</strong> spends the most effort where a failure is both <strong>likely</strong> and <strong>harmful</strong>:</p>
<p><img src="https://provalab.io/blog/basics/risk-matrix.svg" alt="A risk matrix of likelihood against impact. High impact and high likelihood, such as payment and discount totals: test first and deepest. High impact, low likelihood, such as sign-in: test well and automate. Low impact, high likelihood, such as a new promo banner: test the main flows. Low and low, such as help page text: a quick check." width="880" height="470" loading="lazy"></p>
<p>For each feature, ask two questions:</p>
<ul>
<li><strong>How likely is it to break?</strong> New code, complex code, code that changed a lot, or code with a history of bugs.</li>
<li><strong>How bad is it if it breaks?</strong> Money, personal data, sign-in and anything that stops customers ordering.</li>
</ul>
<p>Put the answers in the plan. They decide the order of testing and where the deepest test cases go, and they tell everyone what was tested lightly if time runs out.</p>
<h2>How to write one, step by step</h2>
<ol>
<li><strong>Read the requirements</strong>, and ask questions (STLC Phase 1).</li>
<li><strong>Write the objective</strong> in one sentence.</li>
<li><strong>List what&#39;s in and out of scope</strong>, with a reason for each &quot;out&quot;.</li>
<li><strong>Rank the risks</strong>, using the matrix above.</li>
<li><strong>Choose the approach</strong>: which levels and types, and what to automate.</li>
<li><strong>Define entry and exit criteria</strong> that can be checked, not &quot;when it looks good&quot;.</li>
<li><strong>Add people, environment and schedule.</strong></li>
<li><strong>Review it</strong> with developers and the product owner. A plan nobody agreed to isn&#39;t a plan.</li>
</ol>
<h2>Test plans in agile teams</h2>
<p>Agile teams rarely write long plans for every sprint. They usually keep:</p>
<ul>
<li><strong>A light test strategy</strong> for the product, updated a few times a year.</li>
<li><strong>Testing notes in each sprint</strong>: acceptance criteria on every user story, and a short checklist of what is risky this sprint.</li>
<li><strong>A short release plan</strong> for bigger releases: scope, risks and exit criteria.</li>
</ul>
<p>The questions stay the same. Only the paperwork shrinks.</p>
<h2>Common mistakes</h2>
<ul>
<li><strong>Copying last release&#39;s plan</strong> without changing the scope or risks.</li>
<li><strong>Vague exit criteria</strong> like &quot;testing is complete&quot;. Use measurable ones.</li>
<li><strong>No &quot;out of scope&quot; section,</strong> so gaps stay invisible.</li>
<li><strong>Writing it alone.</strong> Developers know where the code is fragile; product owners know what matters most to customers.</li>
<li><strong>Never updating it.</strong> When the scope changes mid-release, the plan should too.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a></strong> we look at what happens when a test fails: <strong>how to write a good bug report</strong> that developers can act on quickly.</p>
<p>In ProvaLab Test Management, a test plan has these fields built in: objective, test lead, dates, entry and exit criteria, and the features in scope. Test cases attach to the plan, and every test run reports back into it. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to write good test cases: template, examples and techniques</title>
      <link>https://provalab.io/blog/how-to-write-test-cases/</link>
      <guid isPermaLink="true">https://provalab.io/blog/how-to-write-test-cases/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-cases</category>
      <description>What goes into a test case, a ready-to-use template with real examples, the techniques testers use to pick the right cases, and the mistakes to avoid.</description>
      <content:encoded><![CDATA[<p>Test cases are the building blocks of all testing. Whether a person follows them by hand or code runs them automatically, as in <a href="https://provalab.io/blog/first-automated-test-pytest/">Part 5</a>, every test starts as a test case. In this part you will learn what goes into one, how to write it well, and how to choose <em>which</em> test cases to write.</p>
<h2>What is a test case?</h2>
<p><strong>A test case is a set of steps, with specific data, that checks one thing about the software and says exactly what result to expect.</strong></p>
<p>Anyone on the team should be able to pick it up, follow it and get the same answer: pass or fail. If two testers can read the same test case and do different things, it isn&#39;t finished.</p>
<h2>Test scenario vs test case</h2>
<p>These two get mixed up a lot:</p>
<ul>
<li>A <strong>test scenario</strong> is <em>what</em> to test, in one line: &quot;Check that discount codes work at checkout.&quot;</li>
<li>A <strong>test case</strong> is <em>exactly how</em> to test one part of it: which code, which cart, which clicks, which result.</li>
</ul>
<p>One scenario usually becomes several test cases: a valid code, an expired code, two codes at once, a code on an empty cart, and so on.</p>
<h2>The anatomy of a test case</h2>
<p><img src="https://provalab.io/blog/basics/test-case-anatomy.svg" alt="A test case card, TC-104 Apply an expired discount code. Requirement REQ-12, priority High. Preconditions: signed-in customer, cart total 500. Test data: code OLD10, expired yesterday. Steps: open the cart, enter OLD10, click Apply. Expected result: &quot;This code has expired&quot; is shown and the total stays 500. Actual result and status are filled in when the test is run." width="880" height="560" loading="lazy"></p>
<table>
<thead>
<tr>
<th>Field</th>
<th>What to write</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ID</strong></td>
<td>A unique reference, such as TC-104</td>
</tr>
<tr>
<td><strong>Title</strong></td>
<td>What it checks, in plain words: &quot;Apply an expired discount code&quot;</td>
</tr>
<tr>
<td><strong>Requirement</strong></td>
<td>The requirement or user story it covers (we come back to this in Part 10)</td>
</tr>
<tr>
<td><strong>Priority</strong></td>
<td>How important it is: High, Medium or Low</td>
</tr>
<tr>
<td><strong>Preconditions</strong></td>
<td>What must be true before you start</td>
</tr>
<tr>
<td><strong>Test data</strong></td>
<td>The exact values to use</td>
</tr>
<tr>
<td><strong>Steps</strong></td>
<td>Short, numbered actions</td>
</tr>
<tr>
<td><strong>Expected result</strong></td>
<td>What should happen, precise enough to judge pass or fail</td>
</tr>
<tr>
<td><strong>Actual result</strong></td>
<td>What really happened, filled in during the run</td>
</tr>
<tr>
<td><strong>Status</strong></td>
<td>Pass, Fail, Blocked or Not run</td>
</tr>
</tbody></table>
<h2>Example: discount codes in the food delivery app</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Test data</th>
<th>Expected result</th>
</tr>
</thead>
<tbody><tr>
<td>TC-101</td>
<td>Apply a valid 10% code</td>
<td>SAVE10, cart 500</td>
<td>Total becomes 450</td>
</tr>
<tr>
<td>TC-102</td>
<td>Code is not case-sensitive</td>
<td>save10, cart 500</td>
<td>Total becomes 450</td>
</tr>
<tr>
<td>TC-103</td>
<td>Apply an unknown code</td>
<td>NOPE99</td>
<td>&quot;Code not recognised&quot;; total stays 500</td>
</tr>
<tr>
<td>TC-104</td>
<td>Apply an expired code</td>
<td>OLD10</td>
<td>&quot;This code has expired&quot;; total stays 500</td>
</tr>
<tr>
<td>TC-105</td>
<td>Apply two codes</td>
<td>SAVE10, then FREEDEL</td>
<td>Second code refused with a clear message</td>
</tr>
<tr>
<td>TC-106</td>
<td>Remove all items after applying a code</td>
<td>SAVE10, then empty the cart</td>
<td>Discount removed; total 0</td>
</tr>
</tbody></table>
<p>Notice the mix: one <strong>positive</strong> case (it works when used correctly) and several <strong>negative</strong> cases (it fails safely when used wrongly). Most real bugs live in the negative cases, so don&#39;t skip them.</p>
<h2>Techniques for choosing test cases</h2>
<p>You can&#39;t test every possible input. These techniques help you pick the few cases that catch the most bugs.</p>
<h3>Equivalence partitioning</h3>
<p>Group inputs that the software should treat the same way, then test <strong>one value from each group</strong>. If a discount field accepts 0 to 100, there are three groups: below 0, 0 to 100, and above 100. Testing 50 tells you about the whole valid group; you don&#39;t need to test 51, 52 and 53.</p>
<h3>Boundary value analysis</h3>
<p>Bugs love edges. A developer might write <code>&lt;</code> where they meant <code>&lt;=</code>, and only the exact edge value shows it. So test <strong>at and around each boundary</strong>:</p>
<p><img src="https://provalab.io/blog/basics/boundary-values.svg" alt="A discount field accepting 0 to 100 percent. Equivalence classes: below 0 invalid, 0 to 100 valid, above 100 invalid. Boundary values to test: −1, 0, 1, 99, 100 and 101. −1 and 101 must be refused; the other four accepted." width="880" height="330" loading="lazy"></p>
<p>Six well-chosen values catch most of the mistakes a hundred random ones would.</p>
<h3>Decision tables</h3>
<p>When the result depends on <strong>combinations</strong> of conditions, list every combination in a table so none is missed. For free delivery:</p>
<table>
<thead>
<tr>
<th>New customer?</th>
<th>Order over 300?</th>
<th>Free delivery?</th>
</tr>
</thead>
<tbody><tr>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Yes</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>No</td>
<td>No</td>
<td>No</td>
</tr>
</tbody></table>
<p>Each row becomes one test case.</p>
<h3>Error guessing</h3>
<p>Use experience to guess where things break: empty fields, very long text, special characters like <code>&#39;</code> or <code>&lt;</code>, double-clicking a button, going back in the browser, or losing the network mid-payment. Experienced testers keep a personal list of these.</p>
<h2>Seven habits of good test cases</h2>
<ol>
<li><strong>One test, one purpose.</strong> If the title needs &quot;and&quot;, split it.</li>
<li><strong>A clear, specific title.</strong> &quot;Apply an expired discount code&quot;, not &quot;Discount test 3&quot;.</li>
<li><strong>Exact test data.</strong> &quot;Code OLD10&quot;, not &quot;an expired code&quot;.</li>
<li><strong>Short steps anyone can follow,</strong> without knowing the app.</li>
<li><strong>A checkable expected result.</strong> &quot;Total stays 500&quot;, not &quot;it works correctly&quot;.</li>
<li><strong>Independent.</strong> Each test sets up what it needs, so the order doesn&#39;t matter.</li>
<li><strong>Linked to a requirement,</strong> so you know why it exists and what it covers.</li>
</ol>
<h2>Common mistakes</h2>
<ul>
<li><strong>Vague expected results</strong> like &quot;the page should work fine&quot;. Nobody can fail that test.</li>
<li><strong>Only happy paths.</strong> Users mistype, double-click and go back. Test that too.</li>
<li><strong>Giant test cases</strong> with 30 steps. When step 17 fails, the rest are useless.</li>
<li><strong>Copy-paste duplicates</strong> that drift apart over time. Reuse and update instead.</li>
<li><strong>Never reviewing them.</strong> A quick review by another tester catches missing cases early.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Part 7</a></strong> we go deeper into <strong>test design techniques</strong>: black-box, white-box and grey-box testing, and how each one finds different bugs.</p>
<p>In ProvaLab Test Management, every test case has these fields built in, is grouped into suites, links to its requirement, and keeps the history of every run, manual or automated. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Integration testing explained: approaches, stubs, drivers and examples</title>
      <link>https://provalab.io/blog/integration-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/integration-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>integration-testing</category>
      <description>What integration testing is, why bugs hide where components meet, the big bang, top-down, bottom-up and sandwich approaches, stubs and drivers, and what to check at every integration point.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/types-of-software-testing/">Part 2</a> we met the four levels of testing. In <a href="https://provalab.io/blog/first-automated-test-pytest/">Part 5</a> we wrote unit tests that check one function on its own. But real software is many pieces talking to each other, and <strong>a surprising number of bugs live not inside the pieces, but in the gaps between them.</strong></p>
<p>Finding those bugs is the job of <strong>integration testing</strong>.</p>
<h2>What is integration testing?</h2>
<p><strong>Integration testing checks that separately built parts of the software work correctly together.</strong> The parts might be code modules, services, a database, or an outside system like a payment provider.</p>
<p>Each part may pass all its own unit tests and the whole thing can still fail, because the parts disagree about something: a field name, a date format, a unit of money, or what happens when one of them is slow.</p>
<h2>Where integration bugs hide</h2>
<p>Here is our food delivery app, broken into its main parts:</p>
<p><img src="https://provalab.io/blog/basics/integration-points.svg" alt="Integration points in a food delivery app: the mobile app talks to the order service; the order service talks to the database, the payment service and the restaurant notifier; the payment service talks to an outside payment provider. Unit tests check the boxes; integration tests check the arrows." width="880" height="430" loading="lazy"></p>
<p><strong>Unit tests check the boxes. Integration tests check the arrows.</strong></p>
<p>A classic example: the order service sends the payment amount as <strong>450</strong> meaning rupees, but the payment service expects the amount in <strong>paise</strong> (hundredths of a rupee), so it charges 4.50 instead of 450. Both services pass their unit tests perfectly. Only a test that runs them <em>together</em> catches the bug.</p>
<p>Other typical integration bugs:</p>
<ul>
<li><strong>Different field names or formats:</strong> one side sends <code>orderId</code>, the other expects <code>order_id</code>; one sends dates as <code>29/10/2026</code>, the other expects <code>2026-10-29</code>.</li>
<li><strong>Missing error handling:</strong> the payment service is down, and the order service crashes instead of showing &quot;payment failed, please try again&quot;.</li>
<li><strong>Timeouts:</strong> the restaurant notifier takes 30 seconds and the app gives up after 10.</li>
<li><strong>Wrong assumptions:</strong> the order service assumes the database always returns orders newest first, but nobody told the database that.</li>
<li><strong>Changes on one side:</strong> a team renames a field in their API, and the services that depend on it break.</li>
</ul>
<h2>Approaches to integration testing</h2>
<p>When there are many parts, in what order do you connect and test them?</p>
<p><img src="https://provalab.io/blog/basics/integration-approaches.svg" alt="Three ways to integrate. Big bang: join everything, then test; quick to start, but it is hard to find which join broke. Top-down: start at the top, replacing missing lower parts with stubs that give fixed answers. Bottom-up: start at the bottom, with small test programs called drivers calling the parts that are ready. Sandwich integration does both at once." width="880" height="470" loading="lazy"></p>
<h3>Big bang integration</h3>
<p><strong>Connect everything at once, then test the whole thing.</strong></p>
<ul>
<li><strong>Good:</strong> simple; nothing extra to build.</li>
<li><strong>Bad:</strong> when a test fails, the bug could be in any of the connections, so finding it is slow. And you can&#39;t start until every part is finished.</li>
</ul>
<p>It is only reasonable for very small systems.</p>
<h3>Incremental integration</h3>
<p><strong>Connect and test the parts a few at a time</strong>, so when something fails you know it is in the piece you just added. There are three ways to do this.</p>
<h4>Top-down</h4>
<p>Start with the <strong>top-level part</strong> (for example, the order service) and add the parts below it one by one. Parts that aren&#39;t ready yet are replaced by <strong>stubs</strong>.</p>
<ul>
<li><strong>Good:</strong> the main flow of the application is tested early.</li>
<li><strong>Bad:</strong> many stubs to write; the lowest-level parts are tested last.</li>
</ul>
<h4>Bottom-up</h4>
<p>Start with the <strong>lowest-level parts</strong> (for example, the database layer and the payment client) and work upwards. The parts above that don&#39;t exist yet are replaced by <strong>drivers</strong>.</p>
<ul>
<li><strong>Good:</strong> the foundations are solid before anything is built on them.</li>
<li><strong>Bad:</strong> the full user flow can only be seen at the end.</li>
</ul>
<h4>Sandwich (hybrid)</h4>
<p>Do top-down and bottom-up <strong>at the same time</strong>, meeting in the middle. It is the most common choice for large systems, because different teams can work in parallel.</p>
<h2>Stubs and drivers (and other test doubles)</h2>
<p>When you test part of a system, the parts it depends on may not be ready, or may be slow, costly or unreliable (no one wants a test that charges a real card). So we replace them with stand-ins, called <strong>test doubles</strong>:</p>
<table>
<thead>
<tr>
<th>Test double</th>
<th>What it does</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Stub</strong></td>
<td>Stands in for a part that is <em>called</em>; returns fixed answers</td>
<td>A fake payment service that always replies &quot;payment approved&quot;</td>
</tr>
<tr>
<td><strong>Driver</strong></td>
<td>Stands in for a part that <em>calls</em>; sends test requests</td>
<td>A small script that calls the order service with sample orders</td>
</tr>
<tr>
<td><strong>Mock</strong></td>
<td>A stub that also checks <em>how</em> it was called</td>
<td>Checks the order service called payment exactly once, with 450</td>
</tr>
<tr>
<td><strong>Fake</strong></td>
<td>A simple working version of a real part</td>
<td>An in-memory database instead of the real one</td>
</tr>
</tbody></table>
<p>Test doubles make tests fast and repeatable. But remember: <strong>a test against a stub only proves your code works with the stub.</strong> At some point you also need tests against the real thing, or against a realistic copy in a test environment.</p>
<h2>Two kinds of integration testing</h2>
<ul>
<li><strong>Component integration testing</strong> checks parts <em>inside</em> your system: the order service with your own database and payment service.</li>
<li><strong>System integration testing (SIT)</strong> checks your system with <em>outside</em> systems: the real payment provider&#39;s sandbox, an SMS gateway, a maps API.</li>
</ul>
<p>System integration tests are usually slower and run less often, because outside systems can be slow, rate-limited or briefly unavailable.</p>
<h2>What to check at every integration point</h2>
<p>For each arrow in your diagram, check:</p>
<ol>
<li><strong>The happy path:</strong> a normal request gets the right response, and data arrives correctly on the other side.</li>
<li><strong>Data format and units:</strong> names, types, dates, currencies, time zones and character encoding all match.</li>
<li><strong>Errors:</strong> what happens when the other side returns an error, is down, or sends something unexpected.</li>
<li><strong>Timeouts and slowness:</strong> the caller waits a sensible time and recovers cleanly.</li>
<li><strong>Retries and duplicates:</strong> if a request is retried, does the customer get charged twice?</li>
<li><strong>Security:</strong> the call is authenticated, and no secrets or personal data leak in the messages or logs.</li>
</ol>
<p>Teams with many services also use <strong>contract testing</strong> (with tools such as Pact): each service writes down what it expects from the others, and both sides test against that agreement, so a change on one side can&#39;t silently break the other.</p>
<h2>Integration testing in practice</h2>
<ul>
<li>Integration tests usually run <strong>in the CI pipeline</strong>, after the unit tests pass.</li>
<li>Tools like <strong>Testcontainers</strong> start a real database or message queue in a container just for the tests, then throw it away.</li>
<li><strong>API tests</strong> (with tools like Postman or pytest with requests) are a natural way to test integration points directly.</li>
<li>Keep them <strong>fewer than unit tests and more than end-to-end tests</strong>, as the <a href="https://provalab.io/blog/types-of-software-testing/">testing pyramid</a> suggests.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/system-testing/">Part 13</a></strong> we move up a level to <strong>system testing</strong>: testing the whole application, end to end, the way a real user would.</p>
<p>ProvaLab runs integration and API tests on every change, records each result next to its test case, and sends failures back to your coding agent with the request, the response and the logs. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Manual vs automated testing: what to automate, and what not to</title>
      <link>https://provalab.io/blog/manual-vs-automated-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/manual-vs-automated-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-automation</category>
      <description>When a person should test and when a machine should, how to decide what to automate, what automation really costs, and the tools teams use.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/types-of-software-testing/">Part 2</a> we covered the four levels of testing. Every one of those tests can be done in two ways: <strong>by a person</strong>, or <strong>by code</strong>. Choosing well between the two is one of the most useful skills in testing, and one of the most common places teams go wrong.</p>
<h2>What is manual testing?</h2>
<p><strong>In manual testing, a person uses the software and checks the result themselves.</strong> They click, type, look and decide whether what they see is right.</p>
<p>There are two main styles:</p>
<ul>
<li><strong>Scripted manual testing:</strong> the tester follows written test cases step by step. &quot;Open the cart. Apply code SAVE10. Check the total drops by 10%.&quot;</li>
<li><strong>Exploratory testing:</strong> the tester explores freely, using experience and curiosity. &quot;What happens if I apply two discount codes? Or change the quantity to minus one?&quot;</li>
</ul>
<p>Manual testing is quick to start. You need no code, just the app and a sharp eye.</p>
<h2>What is automated testing?</h2>
<p><strong>In automated testing, code performs the checks.</strong> A test script opens the app, does the steps, and compares the result with what was expected, without a person watching.</p>
<pre><code class="language-python">def test_discount_code_reduces_total(cart):
    cart.add(&quot;Margherita pizza&quot;, price=200)
    cart.apply_code(&quot;SAVE10&quot;)
    assert cart.total == 180
</code></pre>
<p>Once written, that check runs in a second, as often as you like: on every code change, every night, on every browser. That repeatability is the whole point.</p>
<h2>Where each one shines</h2>
<p><img src="https://provalab.io/blog/basics/manual-vs-automated.svg" alt="Manual testing discovers what you didn't know: exploring new features, judging design and ease of use, testing like a curious user. Automated testing checks what you already know, again and again: regression checks on every change, the same flow on many browsers, many data combinations." width="880" height="430" loading="lazy"></p>
<p>The key idea: <strong>automated tests mostly guard what you already know works.</strong> They are excellent at catching <strong>regressions</strong>, things that used to work and broke. <strong>Most new kinds of bugs are found by people</strong>, exploring in ways nobody wrote a script for.</p>
<h2>Side by side</h2>
<table>
<thead>
<tr>
<th></th>
<th>Manual</th>
<th>Automated</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Cost to start</strong></td>
<td>Low: just the app and a person</td>
<td>Higher: someone has to write the code</td>
</tr>
<tr>
<td><strong>Cost to repeat</strong></td>
<td>The same effort every time</td>
<td>Nearly free once written</td>
</tr>
<tr>
<td><strong>Speed</strong></td>
<td>Hours or days for a big suite</td>
<td>Minutes, even for thousands of checks</td>
</tr>
<tr>
<td><strong>Consistency</strong></td>
<td>People get tired and skip steps</td>
<td>Exactly the same steps every time</td>
</tr>
<tr>
<td><strong>Finds new kinds of bugs</strong></td>
<td>Yes, especially exploratory testing</td>
<td>Rarely; it checks what it was told</td>
</tr>
<tr>
<td><strong>Judges design and ease of use</strong></td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td><strong>Needs upkeep</strong></td>
<td>Test cases need updating</td>
<td>Test code needs updating when the app changes</td>
</tr>
</tbody></table>
<h2>What should you automate?</h2>
<p>Ask three questions about any test:</p>
<p><img src="https://provalab.io/blog/basics/should-i-automate.svg" alt="Should I automate this test? Question 1: will you run it again and again? If no, test it manually. Question 2: is the feature stable? If no, test manually now and automate later. Question 3: is the result a clear pass or fail? If no, keep it manual because it needs human judgement. Yes to all three: automate it." width="880" height="520" loading="lazy"></p>
<p><strong>Good candidates for automation:</strong></p>
<ul>
<li><strong>Regression tests:</strong> everything that must keep working after each change.</li>
<li><strong>Smoke tests:</strong> &quot;does the app start, can people sign in?&quot; before every release.</li>
<li><strong>Critical journeys:</strong> sign-up, sign-in, checkout and payment.</li>
<li><strong>Cross-browser and device checks:</strong> the same flow on Chrome, Safari, Android and iPhone.</li>
<li><strong>Data-heavy tests:</strong> the same form with a hundred different inputs.</li>
<li><strong>API tests:</strong> fast, stable and easy to check.</li>
</ul>
<p><strong>Better kept manual:</strong></p>
<ul>
<li><strong>Brand-new features</strong> still changing every week.</li>
<li><strong>Usability and design:</strong> &quot;is this confusing?&quot; needs a person.</li>
<li><strong>Exploratory testing:</strong> looking for the bugs nobody imagined.</li>
<li><strong>One-off checks</strong> you will never repeat.</li>
</ul>
<h2>Automation isn&#39;t free</h2>
<p>Two hidden costs catch many teams out:</p>
<ol>
<li><strong>Maintenance.</strong> When the app changes, tests that depend on it must change too. A button renamed from &quot;Buy&quot; to &quot;Checkout&quot; can break dozens of tests at once.</li>
<li><strong>Flaky tests.</strong> A flaky test sometimes passes and sometimes fails without any code change, often because of timing or test data. Flaky tests are dangerous: once people stop trusting red results, they start ignoring real failures too.</li>
</ol>
<p>Good automation is written like good code: readable, small and reliable. A few trusted tests beat hundreds that nobody believes.</p>
<h2>Three myths about automation</h2>
<ol>
<li><strong>&quot;Automation will replace manual testers.&quot;</strong> It replaces repetitive <em>checking</em>, not thinking. Exploring, questioning and judging still need people.</li>
<li><strong>&quot;We should automate 100% of our tests.&quot;</strong> Some tests cost more to automate and maintain than they will ever save. Automate where it pays back.</li>
<li><strong>&quot;Once written, automated tests run forever.&quot;</strong> They need the same care as the product code they test.</li>
</ol>
<h2>Common automation tools</h2>
<table>
<thead>
<tr>
<th>What you test</th>
<th>Popular tools</th>
</tr>
</thead>
<tbody><tr>
<td>Unit tests</td>
<td>pytest (Python), JUnit (Java), Jest (JavaScript)</td>
</tr>
<tr>
<td>Web UI</td>
<td>Playwright, Selenium, Cypress</td>
</tr>
<tr>
<td>Mobile apps</td>
<td>Appium, Espresso (Android), XCUITest (iOS)</td>
</tr>
<tr>
<td>APIs</td>
<td>Postman, REST Assured, pytest with requests</td>
</tr>
<tr>
<td>Load and performance</td>
<td>k6, JMeter, Locust</td>
</tr>
</tbody></table>
<p>Automated tests usually run in a <strong>CI pipeline</strong> (continuous integration): every time someone changes the code, the tests run automatically and report the result before the change is merged.</p>
<h2>Where AI fits in</h2>
<p>AI coding agents can now write automated tests from a plain description, run them and read the failures. That makes automation much cheaper to start. It doesn&#39;t change the rules above: you still decide <em>what</em> is worth automating, and a person still explores and judges.</p>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">Part 4</a></strong> we put it all in order with the <strong>Software Testing Life Cycle (STLC)</strong>: the six phases of testing, from the first requirement to the release.</p>
<p>ProvaLab runs your automated tests on real browsers and devices, keeps every result next to its test case, and sends failures back to your coding agent with a video and logs. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>MCP, plugins and skills, explained: how AI agents use ProvaLab</title>
      <link>https://provalab.io/blog/mcp-plugins-and-skills-explained/</link>
      <guid isPermaLink="true">https://provalab.io/blog/mcp-plugins-and-skills-explained/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>mcp</category>
      <category>claude-code</category>
      <category>ai-agents</category>
      <description>What MCP is, how Claude Code plugins and skills fit around it, what changed in the new stateless MCP, and how your coding agent can use ProvaLab through it.</description>
      <content:encoded><![CDATA[<p>Three words keep coming up when people talk about AI coding agents: <strong>MCP</strong>, <strong>plugins</strong> and <strong>skills</strong>. They are related, but they do different jobs. This post explains each one in plain language, shows what changed in the newest version of MCP, and walks through how your agent uses them to work with ProvaLab.</p>
<h2>What is MCP?</h2>
<p>The <strong>Model Context Protocol (MCP)</strong> is an open standard, introduced by Anthropic in November 2024, for connecting AI apps to the tools and data they need.</p>
<p>Before MCP, every AI app needed its own custom integration for every tool. MCP replaces that with one standard plug. A tool is wrapped once as an <strong>MCP server</strong>, and any app that speaks MCP, an <strong>MCP client</strong>, can use it. People often compare it to USB-C: one shape of connector for many devices.</p>
<p><img src="https://provalab.io/blog/mcp/what-is-mcp.svg" alt="An AI app (the MCP client) connects over MCP to several MCP servers, such as ProvaLab, GitHub and a database. Each server offers tools, resources and prompts." width="880" height="460" loading="lazy"></p>
<p>An MCP server can offer three kinds of things:</p>
<ul>
<li><strong>Tools</strong>: actions the model can take, such as &quot;run this test suite&quot; or &quot;create an issue&quot;.</li>
<li><strong>Resources</strong>: data the model can read, such as a file or a test report.</li>
<li><strong>Prompts</strong>: ready-made instructions a user can pick, such as &quot;summarise last night&#39;s run&quot;.</li>
</ul>
<p>The important part: the <strong>model decides</strong> which tool to call and when, based on what you asked. You don&#39;t click buttons in another dashboard; you ask in plain language, and the agent does the work through MCP.</p>
<h2>What is a skill?</h2>
<p>MCP gives an agent <em>access</em> to a tool. It does not teach the agent <em>how to use it well</em>. That is what a <strong>skill</strong> is for.</p>
<p>A skill is a folder with a <code>SKILL.md</code> file: a short name, a one-line description of when to use it, and step-by-step instructions. The agent only sees the name and description until your request matches. Then it loads the full instructions. So you can have many skills installed without filling up the agent&#39;s memory.</p>
<p>For example, ProvaLab&#39;s <code>triage-failures</code> skill tells the agent: read the actual error text before describing a failure, group failures by cause rather than by test, and flag skipped tests. Without it, an agent with the same MCP access might just list twenty red tests that all come from one broken selector.</p>
<h2>What is a plugin?</h2>
<p>A <strong>plugin</strong> is the package that ships it all in one install. A Claude Code plugin can bundle:</p>
<ul>
<li><strong>Skills</strong>: the know-how.</li>
<li><strong>Slash commands</strong>: shortcuts like <code>/provalab-test</code>.</li>
<li><strong>MCP servers</strong>: the connection, configured in a <code>.mcp.json</code> file.</li>
<li>Optionally, sub-agents and hooks.</li>
</ul>
<p><img src="https://provalab.io/blog/mcp/plugin-skills-mcp.svg" alt="The ProvaLab plugin bundles skills (the know-how), slash commands (shortcuts) and an MCP connection (the access). The connection reaches the ProvaLab MCP server, where each product feature is an MCP tool." width="880" height="500" loading="lazy"></p>
<p>In one line: <strong>MCP is the access, skills are the know-how, and the plugin is the box they come in.</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>What it is</th>
<th>What it does</th>
<th>ProvaLab example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>MCP server</strong></td>
<td>A service that speaks the MCP standard</td>
<td>Gives the agent tools and data</td>
<td><code>mcp.provalab.io</code></td>
</tr>
<tr>
<td><strong>MCP tool</strong></td>
<td>One action on that server</td>
<td>Does one job when the agent calls it</td>
<td><code>execute_pytest</code>, <code>tms_get_test_run_failures</code></td>
</tr>
<tr>
<td><strong>Skill</strong></td>
<td>A <code>SKILL.md</code> instruction file</td>
<td>Teaches the agent when and how to use the tools</td>
<td><code>run-tests</code>, <code>load-test</code>, <code>triage-failures</code></td>
</tr>
<tr>
<td><strong>Slash command</strong></td>
<td>A saved prompt</td>
<td>A shortcut you type</td>
<td><code>/provalab-test checkout</code></td>
</tr>
<tr>
<td><strong>Plugin</strong></td>
<td>A package</td>
<td>Installs all of the above at once</td>
<td><code>provalab</code></td>
</tr>
</tbody></table>
<h2>&quot;MCP 1.0&quot; vs &quot;MCP 2.0&quot;: what changed?</h2>
<p>Officially, MCP versions are named by date, not by number. The original specification is <strong>2024-11-05</strong>, and it was revised in 2025-03-26, 2025-06-18 and 2025-11-25. The <strong>2026-07-28</strong> revision changed the protocol&#39;s core design so much that many people call it the next generation of MCP, or &quot;MCP 2.0&quot;. Here, &quot;MCP 1.0&quot; means the original design that ran through 2025-11-25.</p>
<p><img src="https://provalab.io/blog/mcp/mcp-old-vs-new.svg" alt="Left: the original MCP opens a session with an initialize handshake and a session id, and the server can ask questions back on an open stream. Right: in the 2026-07-28 revision every request stands alone, carrying its own version and capabilities; when the server needs more input it returns input_required and the client retries." width="880" height="540" loading="lazy"></p>
<p>The headline change: <strong>MCP became stateless.</strong> In the original design, a client opened a session with an <code>initialize</code> handshake, got a session id, and the server had to remember every connected client. In the 2026-07-28 revision there is no handshake and no session id. Every request carries its own protocol version and capabilities, so any server instance can answer any request. That makes MCP servers much easier to run at scale, behind load balancers and gateways.</p>
<table>
<thead>
<tr>
<th></th>
<th>&quot;MCP 1.0&quot; (original design)</th>
<th>&quot;MCP 2.0&quot; (2026-07-28 revision)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Connection</strong></td>
<td><code>initialize</code> handshake, then a session</td>
<td>No handshake; each request is self-contained</td>
</tr>
<tr>
<td><strong>Session id</strong></td>
<td><code>Mcp-Session-Id</code> header ties calls together</td>
<td>Removed; tools pass their own handles if they need state</td>
</tr>
<tr>
<td><strong>Learning what a server can do</strong></td>
<td>During the handshake</td>
<td>Optional <code>server/discover</code> call</td>
</tr>
<tr>
<td><strong>Server needs more input</strong></td>
<td>Server sends a request back on an open stream</td>
<td>Server returns <code>input_required</code>; client retries with the answer (Multi Round-Trip Requests)</td>
</tr>
<tr>
<td><strong>Routing</strong></td>
<td>Gateways must read the request body</td>
<td><code>Mcp-Method</code> and <code>Mcp-Name</code> headers on each HTTP request</td>
</tr>
<tr>
<td><strong>Tool lists</strong></td>
<td>Fetched again and again</td>
<td>Cacheable, with <code>ttlMs</code> and <code>cacheScope</code> hints</td>
</tr>
<tr>
<td><strong>Long-running jobs</strong></td>
<td>Experimental tasks inside the core</td>
<td>Tasks moved to an official extension</td>
</tr>
<tr>
<td><strong>Deprecated</strong></td>
<td>—</td>
<td>Roots, Sampling and Logging, the old HTTP+SSE transport, and Dynamic Client Registration (in favour of Client ID Metadata Documents)</td>
</tr>
</tbody></table>
<p>Deprecated features keep working for at least twelve months under MCP&#39;s new deprecation policy, so existing servers don&#39;t break overnight. Teams that relied on session ids will have some migration work. The details are in the <a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog">official changelog</a> and the <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">release announcement</a>.</p>
<p><strong>What this means for you as a user:</strong> very little changes day to day. You still install a server or plugin and ask in plain language. The difference shows up as faster, more reliable connections, because the servers behind them are simpler to run.</p>
<h2>How to use ProvaLab through MCP</h2>
<p>ProvaLab&#39;s features are exposed as tools on one MCP server at <code>https://mcp.provalab.io/mcp</code>. Running tests, reading results, triaging failures, load tests and security scans can all be driven by your coding agent.</p>
<h3>Option 1: the Claude Code plugin (recommended)</h3>
<p>The plugin gives you the MCP connection plus the skills and slash commands.</p>
<pre><code>/plugin marketplace add provalab-dev/claude-code-plugin
/plugin install provalab@provalab-marketplace
</code></pre>
<p>Then sign in once. The sign-in works on any machine, including SSH sessions and CI, because you approve it in a browser on any device:</p>
<pre><code>curl -fsSL https://plugins.provalab.io/install.sh | sh
provalab-auth login
</code></pre>
<p>Restart Claude Code, then check the connection with <code>claude mcp list</code>. You should see the ProvaLab server marked as connected.</p>
<h3>Option 2: the claude.ai connector</h3>
<p>If you work in claude.ai rather than Claude Code, add the ProvaLab connector instead. It points at the same server and gives you the same tools. Use either the connector or the plugin, not both.</p>
<h3>What you can ask</h3>
<p>Once connected, just ask. The skills pick up your intent:</p>
<ul>
<li><em>&quot;Run the login tests for the checkout project&quot;</em>: runs the suite on ProvaLab&#39;s cloud browsers and records the run in Test Management.</li>
<li><em>&quot;Generate tests for <a href="https://staging.example.com/signup">https://staging.example.com/signup</a> and run them&quot;</em>: writes the tests from the live page, then runs them.</li>
<li><em>&quot;Why did last night&#39;s run fail?&quot;</em>: reads the failures, groups them by cause and explains them.</li>
<li><em>&quot;Load test our API up to 200 users&quot;</em>: writes the load test from your repo, shows the cost, and runs it when you say go.</li>
</ul>
<p>Or use the slash commands: <code>/provalab-test</code>, <code>/provalab-status</code>, <code>/provalab-results</code> and <code>/provalab-load</code>.</p>
<p><img src="https://provalab.io/blog/mcp/provalab-flow.svg" alt="One request end to end: you ask Claude Code to run the checkout tests, the run-tests skill decides how, the agent calls the execute_pytest tool over MCP, ProvaLab runs the tests on cloud browsers and records the run, and the result comes back so the agent can explain or fix the failures." width="880" height="330" loading="lazy"></p>
<h3>A few tips</h3>
<ul>
<li><strong>Set your project.</strong> If your organization has more than one project, set <code>PROVALAB_PROJECT_ID</code> (and <code>PROVALAB_ORG_ID</code> if you belong to several organizations) so results land in the right place.</li>
<li><strong>Let the agent fix, but you approve.</strong> The real gain comes when failures go straight back to the agent that wrote the code. Keep a person on the final approval.</li>
<li><strong>Allow the tools by name.</strong> Plugin tools are named <code>mcp__plugin_provalab_api__&lt;tool&gt;</code>. Use that pattern in your permission rules if you want to skip repeated approval prompts.</li>
</ul>
<h2>The short version</h2>
<ul>
<li><strong>MCP</strong> is the standard plug that lets AI agents use tools.</li>
<li><strong>Skills</strong> teach the agent how to use those tools well.</li>
<li><strong>Plugins</strong> package both into one install.</li>
<li>The <strong>2026-07-28</strong> revision (&quot;MCP 2.0&quot;) made MCP stateless and easier to scale, without changing how you use it.</li>
<li>With the <strong>ProvaLab plugin</strong>, your coding agent can run tests, read results, triage failures and run load tests, all from a plain-language request.</li>
</ul>
<p>Want to see it working? Watch the <a href="https://provalab.io/#stage-infra">two-minute product tour</a> or <a href="https://provalab.io/demo">book a 30-minute demo</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Performance testing and its types: load, stress, spike, soak, volume and scalability</title>
      <link>https://provalab.io/blog/performance-testing-types/</link>
      <guid isPermaLink="true">https://provalab.io/blog/performance-testing-types/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>performance-testing</category>
      <category>load-testing</category>
      <description>What performance testing is, the six main types and when to use each, the metrics that matter (and why averages lie), how to run a performance test step by step, and the common mistakes.</description>
      <content:encoded><![CDATA[<p>Your food delivery app passes every functional test. Then Friday evening arrives, thousands of hungry people open the app at once, pages take ten seconds to load, payments time out, and orders are lost.</p>
<p>The app <em>worked</em>. It just didn&#39;t work <strong>fast enough, for enough people at once</strong>. Finding that out before your customers do is the job of <strong>performance testing</strong>.</p>
<h2>What is performance testing?</h2>
<p><strong>Performance testing checks how fast, stable and scalable a system is under different amounts of use.</strong> It is a <a href="https://provalab.io/blog/system-testing/">non-functional</a> type of testing: it doesn&#39;t ask &quot;does checkout work?&quot; but &quot;does checkout still work, quickly, when 2,000 people use it at the same time?&quot;</p>
<p>It answers questions like:</p>
<ul>
<li>How fast does the app respond with a normal number of users?</li>
<li>How many users can it handle before it slows down or fails?</li>
<li>What happens during a sudden rush?</li>
<li>Does it stay healthy after running for hours or days?</li>
<li>If we add servers, can we serve more users?</li>
</ul>
<h2>The six types of performance testing</h2>
<p>Each type answers a different question, and each has its own shape of load over time:</p>
<p><img src="https://provalab.io/blog/basics/performance-test-types.svg" alt="Six small charts of load over time. Load test: ramp up to the expected peak and hold it. Stress test: keep increasing beyond the peak until the system breaks. Spike test: a sudden jump to many users and back down. Soak test: a moderate, steady load held for hours. Volume test: normal users with a very large amount of data. Scalability test: increase users in steps while adding servers." width="880" height="540" loading="lazy"></p>
<h3>1. Load testing</h3>
<p><strong>Can the system handle the load we expect?</strong></p>
<p>Ramp up to the expected number of users, for example the Friday dinner peak, hold it for a while, and check that response times and error rates stay within your targets. This is the most common performance test, and the one to run before every major release.</p>
<p><em>Example:</em> 2,000 users browsing menus and placing orders for 15 minutes; 95% of pages must load in under 1 second, with less than 1% errors.</p>
<h3>2. Stress testing</h3>
<p><strong>Where does the system break, and how does it break?</strong></p>
<p>Keep increasing the load <strong>beyond</strong> the expected peak until something fails. The goal isn&#39;t to pass; it&#39;s to learn the breaking point and the failure mode. Does the app slow down gracefully and show a friendly &quot;busy, please wait&quot; message, or does it crash and lose orders? Does it recover by itself when the load drops?</p>
<h3>3. Spike testing</h3>
<p><strong>Can the system survive a sudden rush?</strong></p>
<p>Jump from a small load to a very large one in seconds, then back down. Real spikes happen: a push notification about a 50% discount, a TV advert, a cricket match ending. Auto-scaling systems in particular need this test, because adding servers takes time and the spike arrives first.</p>
<h3>4. Soak testing (endurance testing)</h3>
<p><strong>Does the system stay healthy over a long time?</strong></p>
<p>Run a moderate, realistic load for many hours, or even days. Some problems only appear slowly: memory that is never freed (a <strong>memory leak</strong>), database connections that aren&#39;t returned, log files that fill the disk, or caches that grow forever. A one-hour test will never find these.</p>
<h3>5. Volume testing</h3>
<p><strong>Does the system stay fast with a lot of data?</strong></p>
<p>Keep the number of users normal, but fill the system with a large amount of <strong>data</strong>: millions of past orders, a customer with 5,000 orders in their history, a restaurant with a huge menu. Queries that are instant on a small test database can be very slow on a full-sized one.</p>
<h3>6. Scalability testing</h3>
<p><strong>Does adding resources let us serve more users?</strong></p>
<p>Increase the load in steps while adding servers or capacity, and check that the system&#39;s capacity grows with them. If doubling the servers doesn&#39;t come close to doubling the capacity, something, often the database, is a bottleneck that more servers can&#39;t fix.</p>
<h3>The six types at a glance</h3>
<table>
<thead>
<tr>
<th>Type</th>
<th>Question</th>
<th>Load shape</th>
<th>Typical length</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Load</strong></td>
<td>Can it handle the expected load?</td>
<td>Ramp to the expected peak, hold</td>
<td>15–60 minutes</td>
</tr>
<tr>
<td><strong>Stress</strong></td>
<td>Where and how does it break?</td>
<td>Keep increasing past the peak</td>
<td>Until it breaks</td>
</tr>
<tr>
<td><strong>Spike</strong></td>
<td>Does it survive a sudden rush?</td>
<td>Sudden jump, then drop</td>
<td>Minutes</td>
</tr>
<tr>
<td><strong>Soak</strong></td>
<td>Does it stay healthy over time?</td>
<td>Moderate and steady</td>
<td>Hours to days</td>
</tr>
<tr>
<td><strong>Volume</strong></td>
<td>Is it fast with lots of data?</td>
<td>Normal users, huge data</td>
<td>Varies</td>
</tr>
<tr>
<td><strong>Scalability</strong></td>
<td>Do more resources mean more capacity?</td>
<td>Steps up, adding servers</td>
<td>Varies</td>
</tr>
</tbody></table>
<p>You may also hear <strong>capacity testing</strong> (finding the maximum number of users that still meets your targets), which sits between load and stress testing.</p>
<h2>The metrics that matter</h2>
<table>
<thead>
<tr>
<th>Metric</th>
<th>What it means</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Response time</strong></td>
<td>How long a request takes, from sending it to getting the answer</td>
</tr>
<tr>
<td><strong>Throughput</strong></td>
<td>How many requests the system handles per second</td>
</tr>
<tr>
<td><strong>Error rate</strong></td>
<td>The share of requests that fail or time out</td>
</tr>
<tr>
<td><strong>Concurrent users</strong></td>
<td>How many people are using the system at the same moment</td>
</tr>
<tr>
<td><strong>Resource use</strong></td>
<td>CPU, memory, disk and network on the servers, and database connections</td>
</tr>
</tbody></table>
<h3>Why averages lie: use percentiles</h3>
<p>The <strong>average</strong> response time hides your slowest users:</p>
<p><img src="https://provalab.io/blog/basics/percentiles.svg" alt="An example run: most requests take 200 to 400 milliseconds, but the slowest few take 2 to 4 seconds. The average, about 450 ms, looks fine. The p95 is about 1.8 seconds, meaning 1 in 20 users waits that long or longer; the p99 is about 3.5 seconds." width="880" height="380" loading="lazy"></p>
<p>That&#39;s why performance targets use <strong>percentiles</strong>:</p>
<ul>
<li><strong>p50 (median):</strong> half of requests are faster than this.</li>
<li><strong>p95:</strong> 95% of requests are faster than this; 1 in 20 users waits longer.</li>
<li><strong>p99:</strong> 99% are faster; 1 in 100 waits longer.</li>
</ul>
<p>When thousands of people use your app, &quot;1 in 20&quot; is a lot of unhappy customers. A good target sounds like: <strong>&quot;p95 under 1 second, error rate under 1%, at 2,000 users.&quot;</strong></p>
<h2>How to run a performance test, step by step</h2>
<ol>
<li><strong>Set clear goals.</strong> Which pages and APIs, how many users, and what response time and error rate count as a pass.</li>
<li><strong>Model realistic behaviour.</strong> Real users don&#39;t all hit &quot;Pay&quot; at the same millisecond. Mix the journeys (browse, search, order) in realistic proportions, and add <strong>think time</strong>: the pause while a user reads a menu.</li>
<li><strong>Use a production-like environment.</strong> Same server sizes, same configuration and a realistic amount of data. Results from a tiny test server don&#39;t predict production.</li>
<li><strong>Prepare test data.</strong> Enough test accounts, restaurants and codes that thousands of virtual users don&#39;t all fight over the same row in the database.</li>
<li><strong>Write the test script.</strong> Here is a load test written for k6, a popular open-source tool:</li>
</ol>
<pre><code class="language-javascript">import http from &#39;k6/http&#39;;
import { check, sleep } from &#39;k6&#39;;

export const options = {
  stages: [
    { duration: &#39;2m&#39;, target: 200 },  // ramp up to 200 users
    { duration: &#39;5m&#39;, target: 200 },  // hold
    { duration: &#39;1m&#39;, target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: [&#39;p(95)&lt;1000&#39;], // 95% of requests under 1 second
    http_req_failed: [&#39;rate&lt;0.01&#39;],     // less than 1% errors
  },
};

export default function () {
  const res = http.get(&#39;https://staging.example.com/api/restaurants?q=pizza&#39;);
  check(res, { &#39;status is 200&#39;: (r) =&gt; r.status === 200 });
  sleep(1); // think time
}
</code></pre>
<ol start="6">
<li><strong>Run it, and watch the servers too.</strong> Response times tell you <em>that</em> it&#39;s slow; CPU, memory and database graphs tell you <em>why</em>.</li>
<li><strong>Find the bottleneck, fix it, and run it again.</strong> Performance work is a loop.</li>
</ol>
<p><strong>Only ever test systems you own or have permission to test.</strong> Sending heavy load to someone else&#39;s website or API can look like an attack, and can take their service down.</p>
<h2>Common bottlenecks</h2>
<ul>
<li><strong>Slow database queries</strong>, often a missing index.</li>
<li><strong>The &quot;N+1&quot; problem:</strong> loading a list of orders, then making a separate database call for each one.</li>
<li><strong>Too few database connections</strong> in the connection pool.</li>
<li><strong>No caching</strong> of data that rarely changes, like menus.</li>
<li><strong>Slow or rate-limited outside services</strong>, such as a payment provider or a maps API.</li>
<li><strong>Large pages and images</strong> that take long to download on mobile networks.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>Only looking at averages</strong> instead of p95 and p99.</li>
<li><strong>Testing from an overloaded load generator.</strong> If the machine <em>sending</em> the traffic is at 100% CPU, you&#39;re measuring it, not your app.</li>
<li><strong>Unrealistic scenarios:</strong> no think time, every user doing the same thing, or a test database with ten rows.</li>
<li><strong>Testing once, just before launch.</strong> Run load tests regularly, so you see when a change makes things slower.</li>
<li><strong>Ignoring errors</strong> because response times looked good. Fast errors are still errors.</li>
</ul>
<h2>Popular tools</h2>
<table>
<thead>
<tr>
<th>Tool</th>
<th>Good for</th>
</tr>
</thead>
<tbody><tr>
<td><strong>k6</strong></td>
<td>Scripted load tests in JavaScript; good for developers and CI</td>
</tr>
<tr>
<td><strong>Apache JMeter</strong></td>
<td>A long-standing, GUI-based tool with many plugins</td>
</tr>
<tr>
<td><strong>Gatling</strong></td>
<td>High-load tests written as code (Scala, Java, Kotlin)</td>
</tr>
<tr>
<td><strong>Locust</strong></td>
<td>Load tests written in Python</td>
</tr>
</tbody></table>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/test-runs-and-test-results/">Part 17</a></strong> we look at <strong>test runs and test results</strong>: what happens each time a set of tests is run, and how to read the results.</p>
<p>ProvaLab&#39;s load testing runs k6 tests from the cloud, shows response time percentiles, throughput and errors live, and can find your app&#39;s breaking point step by step. Your coding agent can write the test from your code and explain the slow spots afterwards. See it in the <a href="https://provalab.io/#stage-load">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>QA vs QC vs testing, and verification vs validation, explained</title>
      <link>https://provalab.io/blog/qa-vs-qc-verification-vs-validation/</link>
      <guid isPermaLink="true">https://provalab.io/blog/qa-vs-qc-verification-vs-validation/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>quality-assurance</category>
      <description>The difference between quality assurance, quality control and testing, and between verification and validation, with simple definitions, examples, the V-model, and comparison tables.</description>
      <content:encoded><![CDATA[<p>Two pairs of terms cause more confusion in software quality than any others: <strong>QA vs QC</strong> (and where testing fits), and <strong>verification vs validation</strong>. They&#39;re also near the top of every list of QA interview questions.</p>
<p>The short version:</p>
<ul>
<li><strong>QA</strong> improves the <strong>process</strong> to <em>prevent</em> defects. <strong>QC</strong> checks the <strong>product</strong> to <em>find</em> them. <strong>Testing</strong> is the main way QC finds them.</li>
<li><strong>Verification</strong> asks <em>&quot;are we building the product right?&quot;</em> <strong>Validation</strong> asks <em>&quot;are we building the right product?&quot;</em></li>
</ul>
<p>Now the longer version, with examples.</p>
<h2>QA vs QC vs testing</h2>
<p><img src="https://provalab.io/blog/basics/qa-qc-testing.svg" alt="Quality management is the umbrella. Quality assurance focuses on the process and preventing defects: coding and testing standards, definitions of done and checklists, process reviews and audits, training and tools, learning from past defects. Quality control focuses on the product and finding defects: inspections and reviews of finished work, and testing, which means running and examining the software to find defects." width="880" height="440" loading="lazy"></p>
<h3>Quality assurance (QA)</h3>
<p><strong>Quality assurance is about the process.</strong> Its goal is to <strong>prevent</strong> defects by making sure the team works in a way that produces good software in the first place.</p>
<p>QA activities in our food delivery app team might include:</p>
<ul>
<li>Agreeing <strong>coding standards</strong> and a <strong>definition of done</strong> (&quot;a story is done when it has tests, a code review and updated docs&quot;).</li>
<li>Using <strong>templates</strong> for <a href="https://provalab.io/blog/how-to-write-a-test-plan/">test plans</a> and <a href="https://provalab.io/blog/how-to-write-a-bug-report/">bug reports</a>.</li>
<li>Making <strong>code review</strong> mandatory for every change.</li>
<li><strong>Learning from defects:</strong> &quot;we had five payment bugs this quarter; let&#39;s add a payment checklist to code reviews.&quot;</li>
<li><strong>Training</strong> the team, and choosing good tools.</li>
</ul>
<p>QA is <strong>proactive</strong>, runs through the <strong>whole project</strong>, and is everyone&#39;s responsibility.</p>
<h3>Quality control (QC)</h3>
<p><strong>Quality control is about the product.</strong> Its goal is to <strong>find</strong> defects in what has been built, before it reaches users.</p>
<p>QC activities include:</p>
<ul>
<li><strong>Reviewing</strong> finished work: requirements, designs and code.</li>
<li><strong>Testing</strong> the software: everything from <a href="https://provalab.io/blog/first-automated-test-pytest/">unit tests</a> to <a href="https://provalab.io/blog/system-testing/">system tests</a>.</li>
<li><strong>Checking</strong> releases against <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">exit criteria</a> before they ship.</li>
</ul>
<p>QC is <strong>reactive</strong>: it looks at a product that already exists and asks &quot;is this right?&quot;</p>
<h3>Testing</h3>
<p><strong>Testing is one part of quality control:</strong> running and examining the software to find defects and to provide information about its quality. Everything in this series so far, from test cases to regression runs, is testing.</p>
<h3>The differences at a glance</h3>
<table>
<thead>
<tr>
<th></th>
<th>Quality assurance</th>
<th>Quality control</th>
<th>Testing</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Focus</strong></td>
<td>The process</td>
<td>The product</td>
<td>The product</td>
</tr>
<tr>
<td><strong>Goal</strong></td>
<td>Prevent defects</td>
<td>Find defects</td>
<td>Find defects, give information about quality</td>
</tr>
<tr>
<td><strong>Approach</strong></td>
<td>Proactive</td>
<td>Reactive</td>
<td>Reactive</td>
</tr>
<tr>
<td><strong>When</strong></td>
<td>Throughout the project</td>
<td>After something is built</td>
<td>After something is built</td>
</tr>
<tr>
<td><strong>Examples</strong></td>
<td>Standards, reviews of how work is done, training, process improvement</td>
<td>Inspections, reviews of finished work, testing</td>
<td>Running test cases, exploratory testing, automated test runs</td>
</tr>
<tr>
<td><strong>Question</strong></td>
<td>Are we working the right way?</td>
<td>Is this product right?</td>
<td>Does this behave as it should?</td>
</tr>
</tbody></table>
<p>A simple analogy from a restaurant kitchen: <strong>QA</strong> is the recipe, the hygiene rules and the chef&#39;s training. <strong>QC</strong> is tasting each dish before it leaves the kitchen.</p>
<h3>&quot;But my job title is QA engineer...&quot;</h3>
<p>In everyday industry use, &quot;QA&quot; often just means &quot;testing&quot;: a <strong>QA engineer</strong> or <strong>QA team</strong> usually spends most of their time testing. That&#39;s fine in practice. But in interviews and in quality-management standards (such as ISO 9001), QA and QC have the distinct meanings above, so it&#39;s worth knowing the difference.</p>
<h2>Verification vs validation</h2>
<p>The classic definitions, from software engineer Barry Boehm:</p>
<ul>
<li><strong>Verification: &quot;Are we building the product right?&quot;</strong> Does the product meet its <strong>specification</strong>: the requirements, designs and standards written for it?</li>
<li><strong>Validation: &quot;Are we building the right product?&quot;</strong> Does the product meet the <strong>real needs</strong> of its users and the business?</li>
</ul>
<h3>Verification</h3>
<p>Verification checks each piece of work <strong>against what it was supposed to be</strong>. Much of it happens <strong>without running the software</strong>:</p>
<ul>
<li><strong>Reviewing</strong> requirements for gaps and contradictions.</li>
<li><strong>Reviewing</strong> designs against requirements.</li>
<li><strong>Code review</strong> and <strong>static analysis</strong> tools, which check code without running it.</li>
<li><strong>Testing against the specification</strong>: does the discount function return exactly what the requirement says?</li>
</ul>
<p><em>Example:</em> the requirement says &quot;expired codes must be refused with a clear message&quot;. A tester checks that the app does exactly that. That&#39;s verification.</p>
<h3>Validation</h3>
<p>Validation checks the product <strong>against what users actually need</strong>. It usually happens <strong>by running the software with real or realistic use</strong>:</p>
<ul>
<li><strong>Acceptance testing</strong> with the product owner or customers.</li>
<li><strong>Usability testing</strong> with real users.</li>
<li><strong>Beta releases</strong> to a small group of real customers.</li>
<li><strong>Demos</strong> to stakeholders at the end of each sprint.</li>
</ul>
<p><em>Example:</em> the new restaurant dashboard matches its specification perfectly, which is verified. But restaurant owners find it too slow to use during the dinner rush. It fails validation. (This is <a href="https://provalab.io/blog/seven-principles-of-testing/">principle 7</a>: bug-free can still be wrong.)</p>
<h3>The differences at a glance</h3>
<table>
<thead>
<tr>
<th></th>
<th>Verification</th>
<th>Validation</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Question</strong></td>
<td>Are we building the product right?</td>
<td>Are we building the right product?</td>
</tr>
<tr>
<td><strong>Checks against</strong></td>
<td>Requirements, designs, standards</td>
<td>Real user and business needs</td>
</tr>
<tr>
<td><strong>Typical activities</strong></td>
<td>Reviews, inspections, static analysis, testing against the spec</td>
<td>Acceptance testing, usability testing, beta releases, demos</td>
</tr>
<tr>
<td><strong>Software running?</strong></td>
<td>Often not (reviews), sometimes yes</td>
<td>Usually yes</td>
</tr>
<tr>
<td><strong>When</strong></td>
<td>Throughout development</td>
<td>Mostly towards the end, and with every release to users</td>
</tr>
<tr>
<td><strong>Done by</strong></td>
<td>Developers, testers, reviewers</td>
<td>Customers, users, product owners, testers</td>
</tr>
<tr>
<td><strong>Finds</strong></td>
<td>Mistakes in building to the spec</td>
<td>A spec that doesn&#39;t match what users need</td>
</tr>
</tbody></table>
<p><strong>You need both.</strong> Verification without validation can build a perfect version of the wrong product. Validation without verification finds the right product, full of bugs.</p>
<h2>The V-model: where they meet</h2>
<p>The <strong>V-model</strong> shows how each step of building software has a matching level of testing:</p>
<p><img src="https://provalab.io/blog/basics/v-model.svg" alt="The V-model. Down the left side: business requirements, system specification, architecture design and detailed design, down to coding at the bottom. Up the right side, each test level checks a left-side step: unit testing checks the detailed design, integration testing the architecture, system testing the specification, and acceptance testing the business requirements. Verification asks whether we are building the product right; validation, at the top right, asks whether we are building the right product." width="880" height="480" loading="lazy"></p>
<ul>
<li>Each step down the left is <strong>verified</strong> against the one before it, through reviews.</li>
<li>Each test level up the right checks the matching step on the left: <a href="https://provalab.io/blog/first-automated-test-pytest/">unit tests</a> check the detailed design, <a href="https://provalab.io/blog/integration-testing/">integration tests</a> the architecture, <a href="https://provalab.io/blog/system-testing/">system tests</a> the specification.</li>
<li><strong>Acceptance testing</strong>, at the top, <strong>validates</strong> the product against the business&#39;s real needs.</li>
</ul>
<p>The V-model also explains why <a href="https://provalab.io/blog/seven-principles-of-testing/">testing should start early</a>: the acceptance tests can be designed as soon as the business requirements are written, long before any code exists.</p>
<h2>Quick interview answers</h2>
<ul>
<li><strong>QA vs QC?</strong> QA prevents defects by improving the process; QC finds defects by checking the product. Testing is the main part of QC.</li>
<li><strong>Verification vs validation?</strong> Verification: are we building the product right (against the spec)? Validation: are we building the right product (for the users)?</li>
<li><strong>Is a code review QA or QC?</strong> Reviewing a specific piece of code is QC (and verification). <em>Requiring</em> code reviews for every change, and improving how they&#39;re done, is QA.</li>
<li><strong>Can something pass verification but fail validation?</strong> Yes: a product that exactly matches a specification that missed what users need.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/static-testing/">Part 21</a></strong> we look at <strong>static testing</strong>: reviews, walkthroughs, inspections and static analysis, the cheapest way to find bugs, because it happens before the software even runs.</p>
<p>ProvaLab supports both sides: Test Management links requirements to test cases and results for verification, the PR Bot reviews every code change before it&#39;s merged, and acceptance and exploratory results live alongside automated ones, so you can see the whole picture of quality in one place. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Regression testing vs re-testing: what they are and why we do both</title>
      <link>https://provalab.io/blog/regression-testing-vs-retesting/</link>
      <guid isPermaLink="true">https://provalab.io/blog/regression-testing-vs-retesting/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>regression-testing</category>
      <category>retesting</category>
      <description>Why every bug fix needs re-testing, how regression testing catches what a change broke elsewhere, the differences between them, how to choose regression tests, and how to keep a regression suite fast.</description>
      <content:encoded><![CDATA[<p>A developer fixes a bug and says &quot;done&quot;. Two questions follow, and they sound similar but are very different:</p>
<ol>
<li><strong>Is the bug really fixed?</strong></li>
<li><strong>Did the fix break anything else?</strong></li>
</ol>
<p>The first is answered by <strong>re-testing</strong>. The second is answered by <strong>regression testing</strong>. Every fix needs both, and mixing them up is one of the most common ways bugs reach customers.</p>
<h2>What is re-testing?</h2>
<p><strong>Re-testing (also called confirmation testing) means running the exact test that failed again, after the bug has been fixed, to confirm the fix works.</strong></p>
<h3>Why do we re-test?</h3>
<p>Because <strong>a fix is only a claim until someone checks it.</strong> In real teams, &quot;fixed&quot; turns out not to be fixed surprisingly often:</p>
<ul>
<li><strong>The fix is partial.</strong> It works for the case the developer tried, but not for the one in the bug report.</li>
<li><strong>It works in one place but not another.</strong> Remember <a href="https://provalab.io/blog/bug-life-cycle/">BUG-231</a>: the expired-code fix worked on the web, but the Android app had its own copy of the check, so the bug was still there.</li>
<li><strong>The fix never arrived.</strong> It&#39;s on the developer&#39;s machine or in a branch, but not in the build being tested.</li>
<li><strong>The developer misunderstood the bug</strong> and fixed something else.</li>
<li><strong>The environment hid the bug,</strong> so it passed on the developer&#39;s laptop but fails with real test data.</li>
</ul>
<h3>How to re-test</h3>
<ul>
<li>Use the <strong>same steps, the same data and the same kind of environment</strong> as the original bug report.</li>
<li>Test on <strong>every platform the bug affected</strong> (web, Android, iPhone).</li>
<li>Try <strong>one or two small variations</strong> around the bug: another expired code, a code that expired one minute ago.</li>
<li>Then update the bug: <strong>Verified</strong> if it passed, <strong>Reopened</strong> with details if not (see <a href="https://provalab.io/blog/bug-life-cycle/">the bug life cycle</a>).</li>
</ul>
<p>The person who found the bug is usually the best one to re-test it.</p>
<h2>What is regression testing?</h2>
<p><strong>Regression testing means re-running tests that used to pass, after a change, to make sure nothing that worked before is now broken.</strong></p>
<p>A <strong>regression</strong> is something that worked yesterday and doesn&#39;t work today. Regressions happen because software is connected: code is shared, data flows between features, and a change in one place can ripple into others.</p>
<h3>Why do we do regression testing?</h3>
<p>Changes break things in places nobody expected:</p>
<ul>
<li><strong>Shared code:</strong> the fix for BUG-231 moved the discount check to the server. Every app now uses it, including flows nobody touched.</li>
<li><strong>Side effects:</strong> a faster search query returns results in a different order, and the &quot;recommended&quot; section breaks.</li>
<li><strong>Merges:</strong> two developers&#39; correct changes conflict when combined.</li>
<li><strong>Upgrades:</strong> a new version of a library, the database or the browser changes behaviour.</li>
<li><strong>Configuration:</strong> a changed setting in one environment affects features far away.</li>
</ul>
<p>Developers can&#39;t always predict these ripples. Regression tests catch them.</p>
<h2>Re-testing vs regression testing for one fix</h2>
<p><img src="https://provalab.io/blog/basics/retest-vs-regression.svg" alt="After the fix for BUG-231: re-testing runs the one failed test, TC-104, at the centre. Targeted regression checks the ring of nearby features the change could affect: valid codes, cart, checkout totals, free delivery, order history and the Android app. The wider regression suite covers the rest of the app, such as sign-in, search, menu, profile, tracking and the restaurant dashboard, nightly and before every release." width="880" height="420" loading="lazy"></p>
<p>For the BUG-231 fix:</p>
<ul>
<li><strong>Re-test:</strong> TC-104 (expired code refused), on web <strong>and</strong> Android.</li>
<li><strong>Targeted regression:</strong> valid codes still work, cart and checkout totals, free-delivery rule, order history totals, the Android checkout. These all touch the discount code that changed.</li>
<li><strong>Wider regression:</strong> the full suite runs that night, and again before the release.</li>
</ul>
<h2>The differences at a glance</h2>
<table>
<thead>
<tr>
<th></th>
<th>Re-testing</th>
<th>Regression testing</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Question</strong></td>
<td>Is this bug really fixed?</td>
<td>Did the change break anything else?</td>
</tr>
<tr>
<td><strong>What is run</strong></td>
<td>The test(s) that failed</td>
<td>Tests that previously passed</td>
</tr>
<tr>
<td><strong>When</strong></td>
<td>After a specific bug is fixed</td>
<td>After any change: fixes, features, upgrades</td>
</tr>
<tr>
<td><strong>Scope</strong></td>
<td>Narrow: the bug</td>
<td>Wide: the areas around the change, or everything</td>
</tr>
<tr>
<td><strong>Planned in advance?</strong></td>
<td>No, it depends on which bugs were found</td>
<td>Yes, a regression suite is built and maintained</td>
</tr>
<tr>
<td><strong>Good for automation?</strong></td>
<td>Sometimes; often quicker by hand</td>
<td>Yes, it&#39;s the classic case for automation</td>
</tr>
<tr>
<td><strong>Expected result</strong></td>
<td>The test that failed now passes</td>
<td>Tests that passed still pass</td>
</tr>
</tbody></table>
<p>Re-testing usually comes <strong>first</strong>: there&#39;s no point running a big regression suite on a fix that doesn&#39;t work. (A quick <a href="https://provalab.io/blog/smoke-vs-sanity-testing/">sanity check</a> often comes before both.)</p>
<h2>How to choose which regression tests to run</h2>
<p>Running every test after every change is the safest choice, but rarely practical. There are three common strategies:</p>
<ol>
<li><strong>Retest all:</strong> run the whole regression suite. Most thorough, slowest. Good before a release.</li>
<li><strong>Regression test selection:</strong> run only the tests related to what changed. A <a href="https://provalab.io/blog/requirements-traceability-matrix/">traceability matrix</a> and good test tagging (by feature, by area of code) make this possible.</li>
<li><strong>Prioritisation:</strong> run tests in order of risk, with critical journeys first (<a href="https://provalab.io/blog/how-to-write-a-test-plan/">risk-based testing</a>), so the most important results come back first.</li>
</ol>
<p>Most teams combine them with a schedule like this:</p>
<p><img src="https://provalab.io/blog/basics/regression-cadence.svg" alt="Which regression tests to run and when. Every commit: unit tests, thousands in about a minute. Every merge or pull request: integration tests and tests for the changed areas, in minutes. Every night: the full automated regression suite of UI, API and system tests on all browsers. Before each release: the full suite plus manual exploratory checks of the risky areas." width="880" height="360" loading="lazy"></p>
<h2>Building and maintaining a regression suite</h2>
<p>A regression suite is a living thing. Some habits that keep it useful:</p>
<ul>
<li><strong>Add a test for every bug you fix.</strong> If a bug happened once, it can come back. The test from the BUG-231 fix joins the suite permanently.</li>
<li><strong>Automate it.</strong> Regression tests are run again and again, which is exactly when automation pays off (see <a href="https://provalab.io/blog/manual-vs-automated-testing/">Part 3</a>).</li>
<li><strong>Tag tests</strong> by feature, priority and speed, so you can run &quot;all checkout tests&quot; or &quot;critical tests only&quot;.</li>
<li><strong>Remove or update obsolete tests.</strong> A test for a feature that no longer exists only slows everyone down.</li>
<li><strong>Fix flaky tests quickly.</strong> If people learn to ignore red results, real regressions slip through.</li>
<li><strong>Watch the run time.</strong> If the suite gets too slow to run often, split it into faster and slower layers.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>Re-testing only on one platform</strong> when the bug affected several.</li>
<li><strong>Skipping regression for &quot;small&quot; fixes.</strong> Small changes to shared code cause big regressions.</li>
<li><strong>Running the full suite on a fix that hasn&#39;t been re-tested,</strong> then wasting the run when the fix turns out not to work.</li>
<li><strong>Never adding tests for fixed bugs,</strong> so the same bug returns months later.</li>
<li><strong>Treating regression testing as manual work forever.</strong> It grows with every release; without automation it becomes impossible to keep up.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/performance-testing-types/">Part 16</a></strong> we look at <strong>performance testing and its types</strong>: load, stress, spike, soak, volume and scalability testing.</p>
<p>ProvaLab runs your regression suite on every change and every night, on real browsers and devices, and shows each test&#39;s history: when it last passed, and the build where it started failing. When a regression appears, your coding agent gets the failure with its logs and recording, and can fix it. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The requirements traceability matrix (RTM): prove everything is tested</title>
      <link>https://provalab.io/blog/requirements-traceability-matrix/</link>
      <guid isPermaLink="true">https://provalab.io/blog/requirements-traceability-matrix/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>traceability</category>
      <category>test-coverage</category>
      <description>What a traceability matrix is, how to build one step by step, forward and backward traceability, how to measure coverage, and how it answers &quot;are we ready to release?&quot;</description>
      <content:encoded><![CDATA[<p>You have <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">requirements</a>, <a href="https://provalab.io/blog/how-to-write-test-cases/">test cases</a>, <a href="https://provalab.io/blog/how-to-write-a-test-plan/">a test plan</a>, test runs and <a href="https://provalab.io/blog/how-to-write-a-bug-report/">bug reports</a>. Each is useful on its own. But on release day, someone will ask the questions that need all of them at once:</p>
<ul>
<li><strong>Is every requirement tested?</strong></li>
<li><strong>Which requirements are failing right now?</strong></li>
<li><strong>This requirement just changed: which tests do we re-run?</strong></li>
</ul>
<p>The tool that answers all three is the <strong>requirements traceability matrix</strong>, usually just called the <strong>RTM</strong> or <strong>traceability matrix</strong>. In the final part of this series, we build one.</p>
<h2>What is a traceability matrix?</h2>
<p><strong>A traceability matrix is a table that links each requirement to the test cases that check it, and to their latest results and bugs.</strong></p>
<p>&quot;Traceability&quot; means you can <strong>trace</strong> a line from any requirement to its tests, and from any test or bug back to its requirement. The matrix makes those lines visible.</p>
<h2>An example</h2>
<p>Here are the discount-code requirements from our food delivery app, matched against the test cases from <a href="https://provalab.io/blog/how-to-write-test-cases/">Part 6</a>:</p>
<p><img src="https://provalab.io/blog/basics/rtm-matrix.svg" alt="A traceability matrix with requirements as rows and test cases TC-101 to TC-106 as columns. REQ-10 apply a valid code: TC-101 and TC-102 passed. REQ-11 refuse unknown codes: TC-103 passed. REQ-12 refuse expired codes: TC-104 failed, linked to BUG-231. REQ-13 one code per order: TC-105 passed. REQ-14 codes in the Android app: no test case, a coverage gap. TC-106 is linked to no requirement." width="880" height="480" loading="lazy"></p>
<p>The same information as a table, the form most teams keep it in:</p>
<table>
<thead>
<tr>
<th>Requirement</th>
<th>Description</th>
<th>Priority</th>
<th>Test cases</th>
<th>Latest result</th>
<th>Bugs</th>
</tr>
</thead>
<tbody><tr>
<td>REQ-10</td>
<td>Apply a valid discount code</td>
<td>High</td>
<td>TC-101, TC-102</td>
<td>Passed</td>
<td>—</td>
</tr>
<tr>
<td>REQ-11</td>
<td>Refuse unknown codes</td>
<td>Medium</td>
<td>TC-103</td>
<td>Passed</td>
<td>—</td>
</tr>
<tr>
<td>REQ-12</td>
<td>Refuse expired codes</td>
<td>High</td>
<td>TC-104</td>
<td><strong>Failed</strong></td>
<td>BUG-231</td>
</tr>
<tr>
<td>REQ-13</td>
<td>Only one code per order</td>
<td>Medium</td>
<td>TC-105</td>
<td>Passed</td>
<td>—</td>
</tr>
<tr>
<td>REQ-14</td>
<td>Codes work in the Android app</td>
<td>High</td>
<td><strong>None</strong></td>
<td><strong>Not tested</strong></td>
<td>—</td>
</tr>
</tbody></table>
<p>At a glance, the matrix shows three problems that would otherwise stay hidden:</p>
<ol>
<li><strong>A coverage gap:</strong> REQ-14 has no tests at all. Remember the Android bug from <a href="https://provalab.io/blog/bug-life-cycle/">Part 10</a>? Without the matrix, nobody would notice that Android was never tested.</li>
<li><strong>A failing requirement:</strong> REQ-12 is tested, but its test fails. The release isn&#39;t ready until BUG-231 is fixed.</li>
<li><strong>An orphan test:</strong> TC-106 (&quot;removing all items removes the discount&quot;) isn&#39;t linked to any requirement. Either the behaviour matters and a requirement is missing, or the test is out of date.</li>
</ol>
<h2>Forward, backward and bidirectional traceability</h2>
<p><img src="https://provalab.io/blog/basics/traceability-chain.svg" alt="Traceability works both ways along the chain requirement REQ-12, test case TC-104, test result failed on 2.4.0, and bug BUG-231. Forward: is every requirement tested, and passing? Backward: why does this test exist, and which requirement does this bug hurt?" width="880" height="300" loading="lazy"></p>
<ul>
<li><strong>Forward traceability</strong> goes from requirements to tests: <em>is every requirement covered, and is it passing?</em> This finds <strong>gaps</strong>.</li>
<li><strong>Backward traceability</strong> goes from tests and bugs back to requirements: <em>why does this test exist, and which requirement does this bug affect?</em> This finds <strong>orphans</strong> and shows the business impact of each bug.</li>
<li><strong>Bidirectional traceability</strong> is both at once, and it is what a good RTM gives you.</li>
</ul>
<h2>Measuring coverage</h2>
<p>The matrix turns &quot;are we done?&quot; into numbers:</p>
<ul>
<li><strong>Requirement coverage</strong> = requirements with at least one test ÷ all requirements.
In our example: 4 of 5 = <strong>80%</strong>. REQ-14 is the missing 20%.</li>
<li><strong>Passing requirements</strong> = requirements whose tests all pass ÷ all requirements.
In our example: 3 of 5 = <strong>60%</strong>. REQ-12 is failing and REQ-14 is untested.</li>
</ul>
<p>Weight these by <strong>priority</strong>. Three of the five requirements are high priority, and two of those three have a problem: REQ-12 is failing and REQ-14 is untested. That tells you far more than the percentage does.</p>
<p><strong>Requirement coverage is not the same as code coverage</strong> from <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Part 7</a>. Code coverage asks &quot;which lines of code ran?&quot; Requirement coverage asks &quot;which promises to the user did we check?&quot; You need both.</p>
<h2>Impact analysis: when a requirement changes</h2>
<p>Requirements change all the time. Say the business decides <strong>expired codes should show a &quot;renew your code&quot; link</strong> instead of just an error. The RTM tells you instantly:</p>
<ul>
<li><strong>Which tests to update:</strong> TC-104.</li>
<li><strong>Which bugs are affected:</strong> BUG-231 (the fix must include the new link).</li>
<li><strong>What to re-run before release:</strong> everything linked to REQ-12.</li>
</ul>
<p>Without the matrix, someone has to search through hundreds of test cases and hope they found them all.</p>
<h2>How to build an RTM, step by step</h2>
<ol>
<li><strong>List the requirements</strong> (or user stories), each with a unique ID such as REQ-12.</li>
<li><strong>Write test cases</strong>, each with its own ID (TC-104), as in Part 6.</li>
<li><strong>Link each test case to the requirements it covers.</strong> Do this when you write the test, not weeks later.</li>
<li><strong>Add the latest result</strong> of each test after every run.</li>
<li><strong>Link bugs</strong> to the failing test and the requirement.</li>
<li><strong>Review the gaps</strong> before each release: requirements with no tests, failing high-priority requirements, and orphan tests.</li>
</ol>
<h2>Where the RTM fits in the life cycle</h2>
<p>The RTM isn&#39;t a one-time document. It grows through the whole <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">testing life cycle</a>:</p>
<table>
<thead>
<tr>
<th>STLC phase</th>
<th>What happens to the RTM</th>
</tr>
</thead>
<tbody><tr>
<td>Requirement analysis</td>
<td>Requirements are listed with IDs</td>
</tr>
<tr>
<td>Test case design</td>
<td>Test cases are linked to requirements; gaps found early</td>
</tr>
<tr>
<td>Test execution</td>
<td>Results and bugs are added after every run</td>
</tr>
<tr>
<td>Test closure</td>
<td>The final RTM shows exactly what was covered and what passed</td>
</tr>
</tbody></table>
<p>In agile teams, the &quot;requirements&quot; are usually <strong>user stories and their acceptance criteria</strong>, and the matrix links tests to stories.</p>
<h2>Common mistakes</h2>
<ul>
<li><strong>A spreadsheet nobody updates.</strong> A stale RTM is worse than none, because it gives false confidence. Link tests and results where they live, so the matrix updates itself.</li>
<li><strong>Counting tests instead of requirements.</strong> Two hundred tests on one feature and none on another is not good coverage.</li>
<li><strong>Linking after the fact.</strong> Link each test to its requirement the moment you write it.</li>
<li><strong>Ignoring orphan tests.</strong> They often point to missing requirements, or to wasted effort on tests nobody needs.</li>
<li><strong>Only checking coverage, never results.</strong> &quot;Tested&quot; is not the same as &quot;passing&quot;.</li>
</ul>
<h2>Testing Basics: the whole series</h2>
<p>That completes our <strong>Testing Basics</strong> series. You now know:</p>
<ol>
<li><a href="https://provalab.io/blog/what-is-software-testing/">What software testing is</a>, and why every team needs it</li>
<li><a href="https://provalab.io/blog/types-of-software-testing/">The types and levels of testing</a>, and the testing pyramid</li>
<li><a href="https://provalab.io/blog/manual-vs-automated-testing/">Manual vs automated testing</a>, and what to automate</li>
<li><a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">The software testing life cycle (STLC)</a></li>
<li><a href="https://provalab.io/blog/first-automated-test-pytest/">How to write your first automated test</a> with pytest</li>
<li><a href="https://provalab.io/blog/how-to-write-test-cases/">How to write good test cases</a></li>
<li><a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Black-box, white-box and grey-box testing</a></li>
<li><a href="https://provalab.io/blog/how-to-write-a-test-plan/">How to write a test plan</a></li>
<li><a href="https://provalab.io/blog/how-to-write-a-bug-report/">How to write a good bug report</a></li>
<li><a href="https://provalab.io/blog/bug-life-cycle/">The bug life cycle</a></li>
<li>The requirements traceability matrix (this post)</li>
</ol>
<p>Next we go deeper into specific kinds of testing, starting with <strong><a href="https://provalab.io/blog/integration-testing/">Part 12: integration testing</a></strong>, then system testing, smoke and sanity testing, regression testing and performance testing.</p>
<p>ProvaLab Test Management keeps this matrix up to date for you: requirements and features link to test cases, every manual or automated run records its results, and issues link back to the test and requirement they affect, so coverage reports are always current. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Security testing basics: the OWASP Top 10 (2025) in plain language</title>
      <link>https://provalab.io/blog/security-testing-basics-owasp-top-10/</link>
      <guid isPermaLink="true">https://provalab.io/blog/security-testing-basics-owasp-top-10/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>security-testing</category>
      <category>owasp</category>
      <description>What security testing is, the ten risks in the OWASP Top 10 2025 explained with everyday examples, the six kinds of security testing and which tools do them, and the checks you can add this week.</description>
      <content:encoded><![CDATA[<p>Every test so far in this series asked &quot;does it work?&quot; Security testing asks a different question: <strong>&quot;what happens when someone tries to make it do something it shouldn&#39;t?&quot;</strong></p>
<p>Users mistype. Attackers don&#39;t. They send exactly the input that breaks a check, ask for exactly the record they&#39;re not allowed to see, and try the default password first. Security testing means doing that before they do.</p>
<h2>What is security testing?</h2>
<p><strong>Security testing checks that the software protects its data and its users against misuse</strong>: that people can only see and do what they&#39;re allowed to, that data is protected in storage and in transit, and that the system fails safely when something unexpected happens.</p>
<p>It&#39;s a <a href="https://provalab.io/blog/system-testing/">non-functional</a> kind of testing, and the one with the highest cost of failure. A functional bug annoys a customer. A security bug can expose every customer at once.</p>
<h2>The OWASP Top 10</h2>
<p><strong>OWASP</strong> (the Open Worldwide Application Security Project) is a non-profit that publishes free security guidance. Its best-known document is the <strong>OWASP Top 10</strong>: the ten most serious risks to web applications, updated every few years from real-world data. The current edition is <strong>2025</strong>. If you learn one thing about security testing, learn these ten.</p>
<p><img src="https://provalab.io/blog/handson/owasp-top-10.svg" alt="The OWASP Top 10, 2025 edition. A01 Broken Access Control. A02 Security Misconfiguration. A03 Software Supply Chain Failures. A04 Cryptographic Failures. A05 Injection. A06 Insecure Design. A07 Authentication Failures. A08 Software or Data Integrity Failures. A09 Security Logging and Alerting Failures. A10 Mishandling of Exceptional Conditions." width="880" height="560" loading="lazy"></p>
<p>Here is each one, with an example from our food delivery app.</p>
<h3>A01 · Broken Access Control</h3>
<p><strong>Users doing things they&#39;re not allowed to.</strong> Alice reads Bob&#39;s order by changing <code>42</code> to <code>43</code> in the URL. A customer reaches the restaurant dashboard. A regular user calls an admin-only endpoint.</p>
<p>This has been number one for years, because it&#39;s common, easy to get wrong and devastating. The 2025 edition also folds in <strong>server-side request forgery (SSRF)</strong>, where an attacker tricks your server into fetching internal resources on their behalf.</p>
<p><em>You&#39;ve already tested for it:</em> <code>test_customer_cannot_read_someone_elses_order</code> in <a href="https://provalab.io/blog/first-api-test-pytest/">Part 4</a> is an access-control test.</p>
<h3>A02 · Security Misconfiguration</h3>
<p><strong>The software is fine; the settings aren&#39;t.</strong> Debug mode left on in production, so errors show internal details. Default admin passwords never changed. Directory listings enabled. Cloud storage buckets open to the world. Missing security headers.</p>
<p>It climbed from fifth to second in 2025, because modern systems have so many settings to get wrong.</p>
<h3>A03 · Software Supply Chain Failures</h3>
<p><strong>A vulnerability in something you installed becomes your vulnerability.</strong> Your app may be perfect, but it runs on hundreds of libraries, a build tool, a container image and some CI plugins. Any of them can carry a known flaw, or be tampered with. New in 2025, replacing and widening the old &quot;vulnerable and outdated components&quot;.</p>
<h3>A04 · Cryptographic Failures</h3>
<p><strong>Data that should be protected isn&#39;t.</strong> Passwords stored in plain text (or with weak hashing). Card numbers kept when they didn&#39;t need to be. Traffic sent without TLS. Home-made encryption. The fix is mostly &quot;use the standard, well-tested thing and don&#39;t invent your own&quot;.</p>
<h3>A05 · Injection</h3>
<p><strong>Untrusted input gets run as code.</strong> The classic is <strong>SQL injection</strong>: a search box that turns <code>&#39; OR 1=1 --</code> into a database query that returns everything. Also OS command injection, and <strong>cross-site scripting (XSS)</strong>, where a comment containing <code>&lt;script&gt;</code> runs in every other user&#39;s browser.</p>
<p>Injection is almost always prevented the same way: never build queries or commands by gluing strings together; use parameterised queries and output encoding.</p>
<h3>A06 · Insecure Design</h3>
<p><strong>A flaw in the plan, not the code.</strong> The discount feature works exactly as specified, but the specification never limited how many codes one order can use, so a customer stacks ten. Password reset by &quot;mother&#39;s maiden name&quot;. No limit on how many OTPs can be requested. No amount of bug-free code fixes a design that&#39;s wrong. This is what <a href="#three-checks-you-can-do-this-week">threat modelling</a> during design is for.</p>
<h3>A07 · Authentication Failures</h3>
<p><strong>Proving who you are, done badly.</strong> Weak passwords accepted. No rate limit on sign-in, so an attacker tries a million passwords. Session tokens that never expire, or that stay valid after sign-out. &quot;Remember me&quot; implemented with the password in a cookie.</p>
<p><em>Already tested:</em> <code>test_no_token_means_401</code> in Part 4 is the simplest authentication test.</p>
<h3>A08 · Software or Data Integrity Failures</h3>
<p><strong>Trusting code or data without checking it.</strong> Auto-updates with no signature check. Deserialising objects from an untrusted source. A CI pipeline anyone can push to. Closely related to supply chain, but about <em>verification</em>: did this come from where it claims, unchanged?</p>
<h3>A09 · Security Logging &amp; Alerting Failures</h3>
<p><strong>Attacks nobody notices.</strong> Failed sign-ins aren&#39;t logged. Logs exist but nobody looks, and nothing alerts. A breach is discovered months later, from the outside. Renamed in 2025 to stress that logging without <strong>alerting</strong> isn&#39;t enough.</p>
<h3>A10 · Mishandling of Exceptional Conditions</h3>
<p><strong>Errors that fail the wrong way.</strong> An exception in the permission check gets caught and the request continues as if allowed (&quot;fail open&quot;). A crash shows users a stack trace with file paths and library versions. An out-of-memory condition leaves half an order saved. New in 2025.</p>
<p><em>Already tested:</em> every &quot;never a 500&quot; assertion in Part 4 is a small check of this. An API that answers <code>422</code> to bad input, instead of crashing, is handling the exceptional condition.</p>
<h2>The six kinds of security testing</h2>
<p>Different risks need different kinds of testing. Three run on the <strong>code</strong> and three on the <strong>running system</strong>:</p>
<p><img src="https://provalab.io/blog/handson/security-testing-kinds.svg" alt="Six kinds of security testing. On the code, every commit: SAST reads the source without running it (Semgrep, Bandit, CodeQL); SCA checks libraries against known vulnerabilities (pip-audit, npm audit, Trivy); secrets scanning finds committed passwords and keys (gitleaks, TruffleHog). On the running system, nightly or before release: DAST attacks the running app from outside (OWASP ZAP, Burp Suite); API fuzzing sends thousands of malformed and unauthorised requests (Schemathesis, ZAP); a penetration test is a skilled person attacking the system." width="880" height="470" loading="lazy"></p>
<table>
<thead>
<tr>
<th>Kind</th>
<th>Looks at</th>
<th>Finds</th>
<th>Typical tools</th>
<th>When</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SAST</strong> (static application security testing)</td>
<td>Source code, without running it</td>
<td>Injection patterns, unsafe functions, hard-coded secrets</td>
<td>Semgrep, Bandit, CodeQL</td>
<td>Every commit</td>
</tr>
<tr>
<td><strong>SCA</strong> (software composition analysis)</td>
<td>Your dependencies</td>
<td>Libraries with known vulnerabilities (A03)</td>
<td>pip-audit, npm audit, Trivy, Dependabot</td>
<td>Every commit, and daily as new flaws are published</td>
</tr>
<tr>
<td><strong>Secrets scanning</strong></td>
<td>The repository, including history</td>
<td>Committed passwords, API keys, tokens</td>
<td>gitleaks, TruffleHog</td>
<td>Every commit</td>
</tr>
<tr>
<td><strong>DAST</strong> (dynamic application security testing)</td>
<td>The running application, from outside</td>
<td>Injection, misconfiguration, missing headers, auth problems</td>
<td>OWASP ZAP, Burp Suite</td>
<td>Nightly, before release</td>
</tr>
<tr>
<td><strong>API fuzzing</strong></td>
<td>The API, with generated requests</td>
<td>Crashes (500s), missing auth checks, validation gaps</td>
<td>Schemathesis, ZAP API scan</td>
<td>Nightly, before release</td>
</tr>
<tr>
<td><strong>Penetration testing</strong></td>
<td>Everything, by a skilled person</td>
<td>Design flaws and chained attacks tools miss</td>
<td>A person</td>
<td>Before major releases, yearly</td>
</tr>
</tbody></table>
<p>The first three are <a href="https://provalab.io/blog/static-testing/">static testing</a> applied to security: cheap, fast, on every change. DAST and fuzzing are <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">dynamic</a> and mostly black-box or grey-box. A penetration test adds the one thing none of the tools have: creativity.</p>
<h3>Which kind finds which risk</h3>
<table>
<thead>
<tr>
<th>Risk</th>
<th>Best found by</th>
</tr>
</thead>
<tbody><tr>
<td>A01 Access control</td>
<td>API tests you write (as in Part 4), fuzzing, pentest</td>
</tr>
<tr>
<td>A02 Misconfiguration</td>
<td>DAST, configuration scanners, cloud posture tools</td>
</tr>
<tr>
<td>A03 Supply chain</td>
<td>SCA, image scanning, pinned and verified dependencies</td>
</tr>
<tr>
<td>A04 Cryptography</td>
<td>SAST, code review, TLS checkers</td>
</tr>
<tr>
<td>A05 Injection</td>
<td>SAST, DAST, fuzzing</td>
</tr>
<tr>
<td>A06 Insecure design</td>
<td>Threat modelling, design review, pentest</td>
</tr>
<tr>
<td>A07 Authentication</td>
<td>API tests, DAST, pentest</td>
</tr>
<tr>
<td>A08 Integrity</td>
<td>Code review, pipeline hardening, signature checks</td>
</tr>
<tr>
<td>A09 Logging</td>
<td>Review and a drill: do an attack, see if anyone is alerted</td>
</tr>
<tr>
<td>A10 Exceptional conditions</td>
<td>Negative tests, fuzzing, code review of error handling</td>
</tr>
</tbody></table>
<p>Notice how many rows say &quot;API tests you write&quot;. <strong>Ordinary functional and negative tests, written with security in mind, are the first and cheapest layer of security testing.</strong> The 403 test, the 401 test, the &quot;never a 500&quot; assertion: you don&#39;t need a security tool to write those.</p>
<h2>Three checks you can do this week</h2>
<ol>
<li><strong>Add one access-control test per resource.</strong> For every kind of record (orders, profiles, addresses), sign in as one user and request another user&#39;s record. Expect <code>403</code> or <code>404</code>, never <code>200</code>. This single habit covers the number-one risk.</li>
<li><strong>Run a dependency scan.</strong> <code>pip-audit</code> for Python, <code>npm audit</code> for JavaScript, or Trivy for containers. It takes a minute and often finds something.</li>
<li><strong>Do a 30-minute threat model.</strong> Draw the app&#39;s data flows on a whiteboard (what goes where, who can reach it) and ask, for each arrow: <em>what&#39;s the worst thing someone could do here?</em> This is the design-stage activity that catches A06, and no tool replaces it.</li>
</ol>
<h2>Rules of the road</h2>
<ul>
<li><strong>Only test what you own or have written permission to test.</strong> Running a scanner or a fuzzer against someone else&#39;s site is illegal in most countries, and it looks exactly like an attack because it is one.</li>
<li><strong>Test against a test environment</strong>, with test data. A scanner will try to submit forms, create records and trip rate limits.</li>
<li><strong>Fix in order of severity</strong>, not in order found. A scanner&#39;s 200 findings usually contain 3 that matter.</li>
<li><strong>Verify, then report.</strong> Scanners produce false positives. Confirm a finding by hand before raising it, and write it up as a <a href="https://provalab.io/blog/how-to-write-a-bug-report/">bug report</a> with steps, not as &quot;the scanner said so&quot;.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/first-security-scan-owasp-zap/">Part 6</a></strong> we run a real <strong>DAST scan with OWASP ZAP</strong> against the little API from Part 4, read the findings, and sort the real ones from the noise.</p>
<p>ProvaLab AppSec runs web app (DAST), API fuzzing, dependency, secrets, container and code scans against targets you&#39;ve verified you own, and turns the findings into a plain-language fix list ordered by severity, with the false positives you&#39;ve checked marked as such. See it in the <a href="https://provalab.io/#stage-appsec">two-minute product tour</a>.</p>
<p>Sources: the <a href="https://owasp.org/Top10/2025/0x00_2025-Introduction/">OWASP Top 10:2025 introduction</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The 7 principles of software testing, explained with examples</title>
      <link>https://provalab.io/blog/seven-principles-of-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/seven-principles-of-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>testing-principles</category>
      <description>The seven testing principles from the ISTQB syllabus in plain language, each with a real-world example and what it means for how you test day to day.</description>
      <content:encoded><![CDATA[<p>Over 18 parts we have covered a lot of the <em>how</em> of testing. This part steps back to the <em>why</em>: seven principles that experienced testers have learned the hard way, and that explain almost every practice in this series.</p>
<p>They come from the <strong>ISTQB Certified Tester Foundation Level</strong> syllabus, the most widely used testing certification, and they&#39;re a favourite interview topic. More importantly, they change how you test.</p>
<p><img src="https://provalab.io/blog/basics/seven-principles.svg" alt="The seven principles: 1, testing shows bugs are there, not that they are gone. 2, exhaustive testing is impossible. 3, test early: it saves time and money. 4, defects cluster together. 5, tests wear out (the pesticide paradox). 6, testing depends on context. 7, bug-free can still be wrong (the absence-of-defects fallacy)." width="880" height="560" loading="lazy"></p>
<h2>1. Testing shows the presence of defects, not their absence</h2>
<p><strong>Testing can show that bugs are there. It can never prove that there are none.</strong></p>
<p>If 500 tests pass, you know the software works <em>in those 500 situations</em>. You don&#39;t know about the 501st. Every passing test reduces the risk of an unknown bug, but never removes it.</p>
<p><em>Example:</em> the discount-code tests all pass. A week after release, a customer finds that applying a code, changing the delivery address and going back to the cart doubles the discount. No test tried that exact sequence.</p>
<p><strong>What it means for you:</strong> never promise &quot;bug-free&quot;. Report what was tested, what wasn&#39;t, and the risk that remains, as in a good <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">test summary report</a>.</p>
<h2>2. Exhaustive testing is impossible</h2>
<p><strong>Testing every possible input and combination is impossible, except in trivially small cases.</strong></p>
<p>A search box that accepts 10 characters of letters and digits has 36¹⁰ possible inputs: over 3.6 quadrillion. At a thousand tests per second, trying them all would take more than 100,000 years. And that&#39;s one field.</p>
<p><em>Example:</em> the food app has thousands of restaurants, many browsers and phones, two languages, dozens of payment options and endless orders of steps. Testing every combination isn&#39;t hard; it&#39;s impossible.</p>
<p><strong>What it means for you:</strong> choose tests wisely, using <a href="https://provalab.io/blog/how-to-write-a-test-plan/">risk</a> and <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">test design techniques</a> like equivalence partitioning, boundary values and pairwise testing. Good choices find most bugs with a small fraction of the tests.</p>
<h2>3. Early testing saves time and money</h2>
<p><strong>The earlier a defect is found, the cheaper it is to fix.</strong></p>
<p>A mistake caught while reading the requirements is a five-minute conversation. The same mistake found by customers in production means support tickets, an urgent fix, a new release, and lost trust.</p>
<p><em>Example:</em> while reviewing the requirement &quot;codes give 10% off&quot;, a tester asks, &quot;before or after the delivery fee?&quot; Nobody had decided. Settling it then costs nothing; finding it after release means refunds.</p>
<p><strong>What it means for you:</strong> test from day one. Review requirements, write test cases before the code, and run tests on every change. This is the &quot;shift left&quot; idea from <a href="https://provalab.io/blog/what-is-software-testing/">Part 1</a>, and the reason the <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">STLC</a> runs alongside development.</p>
<h2>4. Defects cluster together</h2>
<p><strong>A small number of areas usually contain most of the defects.</strong> This is often linked to the 80/20 (Pareto) rule, though the exact split varies.</p>
<p><img src="https://provalab.io/blog/basics/defect-clustering.svg" alt="An example of bugs found per area of the food app in three months, 74 in total: checkout and payment 34, discount codes 21, search 7, order tracking 5, menus 4, profile 2, help pages 1. Two of the seven areas hold 55 of the 74 bugs." width="880" height="380" loading="lazy"></p>
<p>Bugs cluster where code is <strong>complex</strong>, <strong>changes often</strong>, was <strong>written in a hurry</strong>, or connects to <strong>outside systems</strong>.</p>
<p><strong>What it means for you:</strong> use your bug history. Where bugs were found before, more are likely hiding. Test those areas more deeply, and ask <em>why</em> they&#39;re fragile: often the real fix is simpler code or better tests, not just one more bug report.</p>
<h2>5. Tests wear out (the pesticide paradox)</h2>
<p><strong>If you run the same tests over and over, they eventually stop finding new bugs.</strong></p>
<p>The older name, the <strong>pesticide paradox</strong>, compares this to insects becoming resistant to a pesticide used again and again. Once the bugs your tests can find are fixed, the same tests just keep passing, while new bugs appear in places they don&#39;t look.</p>
<p><em>Example:</em> the checkout regression suite has passed every night for six months. Meanwhile a new &quot;pay later&quot; option was added, and no test touches it.</p>
<p><strong>What it means for you:</strong> keep <a href="https://provalab.io/blog/regression-testing-vs-retesting/">regression tests</a> (they still protect against old bugs returning), but <strong>review and extend them regularly</strong>. Add tests for new features and new risks, vary the test data, and mix in <a href="https://provalab.io/blog/manual-vs-automated-testing/">exploratory testing</a> to find what scripts miss.</p>
<h2>6. Testing is context dependent</h2>
<p><strong>There is no single right way to test. The right approach depends on what you&#39;re testing and why.</strong></p>
<p>A banking app, a mobile game, a hospital system and a startup&#39;s first prototype need very different testing: different risks, rules, depth and speed.</p>
<p><em>Example:</em> for the food app, a bug that charges the wrong amount is critical and gets deep testing; a slightly misaligned icon on the help page is not. A medical device would need formal documentation and strict regulation that a food app doesn&#39;t.</p>
<p><strong>What it means for you:</strong> don&#39;t copy another team&#39;s process blindly. Choose your <a href="https://provalab.io/blog/how-to-write-a-test-plan/">test plan</a>, level of documentation, amount of automation and <a href="https://provalab.io/blog/test-coverage-explained/">coverage</a> targets based on your product&#39;s risks.</p>
<h2>7. The absence-of-defects fallacy</h2>
<p><strong>Finding and fixing lots of bugs doesn&#39;t guarantee success. Software with no known bugs can still be the wrong software.</strong></p>
<p>If the system is hard to use, too slow, or simply doesn&#39;t do what users need, a clean bug list doesn&#39;t help.</p>
<p><em>Example:</em> the new restaurant dashboard passes every test, with zero open bugs. But restaurants refuse to use it, because it takes six clicks to accept an order during the dinner rush.</p>
<p><strong>What it means for you:</strong> test against <strong>users&#39; real needs</strong>, not only the written requirements. That&#39;s what <a href="https://provalab.io/blog/types-of-software-testing/">acceptance testing</a> and usability testing are for, along with talking to real users early.</p>
<h2>The seven principles at a glance</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Principle</th>
<th>In one line</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Testing shows the presence of defects</td>
<td>Passing tests never prove there are no bugs</td>
</tr>
<tr>
<td>2</td>
<td>Exhaustive testing is impossible</td>
<td>Choose tests by risk and technique</td>
</tr>
<tr>
<td>3</td>
<td>Early testing saves time and money</td>
<td>Start testing on day one</td>
</tr>
<tr>
<td>4</td>
<td>Defects cluster together</td>
<td>Test harder where bugs were found</td>
</tr>
<tr>
<td>5</td>
<td>Tests wear out</td>
<td>Review and refresh your tests</td>
</tr>
<tr>
<td>6</td>
<td>Testing is context dependent</td>
<td>Fit the testing to the product and its risks</td>
</tr>
<tr>
<td>7</td>
<td>Absence-of-defects fallacy</td>
<td>Bug-free isn&#39;t enough if it&#39;s the wrong product</td>
</tr>
</tbody></table>
<h2>How the principles connect to this series</h2>
<p>Almost everything in earlier parts follows from these principles:</p>
<ul>
<li><strong>Test design techniques</strong> (Parts 6 and 7) exist because of principle 2.</li>
<li><strong>The STLC and test planning</strong> (Parts 4 and 8) put principle 3 into practice.</li>
<li><strong>Risk-based testing</strong> (Part 8) uses principles 4 and 6.</li>
<li><strong>Regression maintenance</strong> (Part 15) answers principle 5.</li>
<li><strong>Acceptance testing</strong> (Part 2) protects against principle 7.</li>
<li><strong>Coverage</strong> (Part 18) is limited by principles 1 and 2.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/qa-vs-qc-verification-vs-validation/">Part 20</a></strong> we clear up three terms that are often mixed up: <strong>QA, QC and testing</strong>, and <strong>verification vs validation</strong>.</p>
<p>ProvaLab helps put the principles into practice: tests run on every change (principle 3), each test case keeps a history so you can see where failures cluster (principle 4), and coverage reports show which requirements have gone untested (principles 1 and 2). See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Smoke testing vs sanity testing: the difference, with examples</title>
      <link>https://provalab.io/blog/smoke-vs-sanity-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/smoke-vs-sanity-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>smoke-testing</category>
      <category>sanity-testing</category>
      <description>What smoke testing and sanity testing are, how they differ, when to use each, example checklists for a real app, and how to automate them.</description>
      <content:encoded><![CDATA[<p>Imagine the team spends a whole morning preparing to test a new build, and the first tester finds that <strong>nobody can even sign in</strong>. Every other test is now blocked, and the morning is gone.</p>
<p>Two quick checks exist to stop that from happening: <strong>smoke testing</strong> and <strong>sanity testing</strong>. They are among the most confused terms in testing, and a favourite interview question. This post clears them up.</p>
<h2>What is smoke testing?</h2>
<p><strong>A smoke test is a quick, shallow check of the most important features of a new build, to decide whether it is stable enough to test further.</strong></p>
<p>It answers one question: <strong>&quot;Is this build worth testing at all?&quot;</strong></p>
<p>The name comes from hardware: switch a new device on, and if smoke comes out, there&#39;s no point testing anything else. Smoke testing is also called <strong>build verification testing (BVT)</strong> or <strong>confidence testing</strong>.</p>
<p>Key traits:</p>
<ul>
<li>Runs on <strong>every new build</strong>, before any deeper testing.</li>
<li>Covers <strong>the main features, lightly</strong>: wide, not deep.</li>
<li>Takes <strong>minutes</strong>, not hours.</li>
<li>Is usually <strong>scripted and automated</strong>.</li>
<li><strong>Fail = reject the build.</strong> Testing doesn&#39;t start until it&#39;s fixed.</li>
</ul>
<h3>Example smoke test checklist</h3>
<p>For our food delivery app, a smoke test might be just ten checks:</p>
<ol>
<li>The app opens and the home page loads.</li>
<li>A test customer can sign in.</li>
<li>Searching for &quot;pizza&quot; returns restaurants.</li>
<li>A restaurant menu opens.</li>
<li>An item can be added to the cart.</li>
<li>Checkout opens and shows the right total.</li>
<li>A test payment in the sandbox succeeds.</li>
<li>The order appears in &quot;My orders&quot;.</li>
<li>The restaurant dashboard receives the order.</li>
<li>The customer can sign out.</li>
</ol>
<p>Notice that none of these go deep. There&#39;s no discount-code edge case, no closed-restaurant scenario. That&#39;s the job of the full test cycle that comes <strong>after</strong> the smoke test passes.</p>
<h2>What is sanity testing?</h2>
<p><strong>A sanity test is a quick, focused check of one area after a small change or bug fix, to confirm the change makes sense before investing in further testing.</strong></p>
<p>It answers a different question: <strong>&quot;Did this specific change work, and does this area still behave sensibly?&quot;</strong></p>
<p>Key traits:</p>
<ul>
<li>Runs <strong>after a small change</strong>: a bug fix, a minor feature, a configuration change.</li>
<li>Covers <strong>one area, a little deeper</strong>: narrow, not wide.</li>
<li>Takes <strong>minutes</strong>.</li>
<li>Is often <strong>unscripted</strong>: a tester who knows the area checks it quickly.</li>
<li><strong>Fail = back to the developer</strong> before wasting time on full re-testing and regression.</li>
</ul>
<h3>Example sanity test</h3>
<p>The developer has just fixed <strong>BUG-231</strong> from <a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a>: expired discount codes were being accepted. A quick sanity check of the discount area:</p>
<ul>
<li>An expired code (OLD10) is now refused with the right message.</li>
<li>A valid code (SAVE10) still works.</li>
<li>Removing a code restores the full total.</li>
<li>The checkout page still loads normally.</li>
</ul>
<p>If that looks sensible, the tester moves on to the formal <strong>re-test</strong> of the bug and the <strong>regression</strong> run (both covered in Part 15). If the valid code is now broken too, the build goes straight back. No point running 200 regression tests on it.</p>
<h2>Smoke vs sanity: the difference at a glance</h2>
<p><img src="https://provalab.io/blog/basics/smoke-vs-sanity.svg" alt="Smoke testing is wide and shallow: it touches every main feature lightly (sign-in, search, menu, cart, discounts, payment, tracking, profile). Sanity testing is narrow and focused: it goes deeper into one area, such as discounts, after a change there." width="880" height="420" loading="lazy"></p>
<table>
<thead>
<tr>
<th></th>
<th>Smoke testing</th>
<th>Sanity testing</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Question</strong></td>
<td>Is this build stable enough to test?</td>
<td>Did this change work, and is the area sensible?</td>
</tr>
<tr>
<td><strong>When</strong></td>
<td>Every new build</td>
<td>After a small fix or change</td>
</tr>
<tr>
<td><strong>Scope</strong></td>
<td>Wide and shallow: all main features</td>
<td>Narrow: one area</td>
</tr>
<tr>
<td><strong>Depth</strong></td>
<td>Just &quot;does it basically work?&quot;</td>
<td>A bit deeper in that area</td>
</tr>
<tr>
<td><strong>Scripted?</strong></td>
<td>Usually scripted and automated</td>
<td>Often unscripted</td>
</tr>
<tr>
<td><strong>Done by</strong></td>
<td>Testers, or automatically in CI</td>
<td>Usually testers</td>
</tr>
<tr>
<td><strong>If it fails</strong></td>
<td>Reject the whole build</td>
<td>Send the change back</td>
</tr>
<tr>
<td><strong>Related to</strong></td>
<td>Build acceptance</td>
<td>Part of regression and re-testing</td>
</tr>
</tbody></table>
<p><strong>A simple way to remember it:</strong> smoke checks the <strong>whole house</strong> has power; sanity checks that <strong>the one room you just rewired</strong> has working lights.</p>
<p><strong>A word of warning:</strong> teams and textbooks don&#39;t all use these terms the same way. Some glossaries treat &quot;sanity test&quot; as just another name for a smoke test. In day-to-day practice, most teams use the distinction above. If you join a new team, ask what <em>they</em> mean by each.</p>
<h2>Where they fit in the testing process</h2>
<p><img src="https://provalab.io/blog/basics/smoke-sanity-flow.svg" alt="Top: a new build is deployed and a smoke test runs. If it passes, full testing starts; if it fails, the build is rejected. Bottom: a small fix such as BUG-231 is deployed and a sanity test runs. If it passes, the bug is re-tested and regression runs; if it fails, the fix goes back to the developer." width="880" height="380" loading="lazy"></p>
<p>In the <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">testing life cycle</a>, a passing smoke test is usually part of the <strong>entry criteria</strong> for test execution: &quot;build deployed and smoke test passed&quot;.</p>
<h2>Automating smoke tests</h2>
<p>Smoke tests are ideal for automation: few in number, run on every build, and with a clear pass or fail.</p>
<ul>
<li><strong>Run them automatically after every deployment</strong> to a test environment, as a step in the CI/CD pipeline.</li>
<li><strong>Keep them fast:</strong> a few minutes at most. If the smoke suite takes an hour, it has turned into a regression suite.</li>
<li><strong>Keep them stable:</strong> a flaky smoke test blocks every build, so these need to be the most reliable tests you have.</li>
<li><strong>Run them in production too:</strong> after each release, a small set of safe, read-only smoke checks (the home page loads, search works) confirms the release went out cleanly.</li>
</ul>
<p>Sanity tests are usually quicker to do by hand, because each one targets whatever just changed.</p>
<h2>Common mistakes</h2>
<ul>
<li><strong>Skipping the smoke test</strong> &quot;because the change was small&quot;. Small changes break builds too.</li>
<li><strong>Letting the smoke suite grow</strong> until it takes as long as full testing.</li>
<li><strong>Testing deeply during a smoke test.</strong> Its job is go / no-go, not finding every bug.</li>
<li><strong>Treating a sanity pass as &quot;done&quot;.</strong> Sanity only says the change is worth testing properly: re-testing and regression still follow.</li>
</ul>
<h2>What&#39;s next</h2>
<p>After the sanity check, two more things happen to every fix: the bug is <strong>re-tested</strong>, and <strong>regression tests</strong> check that nothing else broke. In <strong><a href="https://provalab.io/blog/regression-testing-vs-retesting/">Part 15</a></strong> we explain <strong>regression testing vs re-testing</strong>, and how to keep regression suites fast.</p>
<p>ProvaLab can run your smoke suite automatically after every deployment, on real browsers and devices, and report the result in seconds, so a broken build is caught before anyone starts testing it. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Software Testing Life Cycle (STLC): all six phases explained</title>
      <link>https://provalab.io/blog/software-testing-life-cycle-stlc/</link>
      <guid isPermaLink="true">https://provalab.io/blog/software-testing-life-cycle-stlc/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>stlc</category>
      <description>The six phases of the STLC in plain language, with what goes in and comes out of each, entry and exit criteria, how STLC fits the SDLC, and how it works in agile teams.</description>
      <content:encoded><![CDATA[<p>So far in this series we have covered <a href="https://provalab.io/blog/what-is-software-testing/">what testing is</a>, <a href="https://provalab.io/blog/types-of-software-testing/">the four levels of testing</a> and <a href="https://provalab.io/blog/manual-vs-automated-testing/">manual vs automated testing</a>. Now we put it in order: <strong>what does a tester actually do, step by step, from the first requirement to the release?</strong></p>
<p>The answer is the <strong>Software Testing Life Cycle</strong>, or <strong>STLC</strong>: six phases that take testing from &quot;what should we test?&quot; to &quot;we&#39;re done, here&#39;s what we found&quot;. It is also one of the most common topics in QA job interviews.</p>
<h2>What is the STLC?</h2>
<p><strong>The STLC is the sequence of activities a team follows to plan, design, run and finish testing.</strong> Each phase has:</p>
<ul>
<li><strong>Entry criteria:</strong> what must be true before the phase can start.</li>
<li><strong>Activities:</strong> the work done in the phase.</li>
<li><strong>Deliverables:</strong> what the phase produces.</li>
<li><strong>Exit criteria:</strong> what must be true before you move on.</li>
</ul>
<p>Here are the six phases at a glance:</p>
<p><img src="https://provalab.io/blog/basics/stlc-phases.svg" alt="The six STLC phases: 1 requirement analysis, producing testable requirements and an RTM; 2 test planning, producing the test plan; 3 test case design, producing test cases and test data; 4 environment setup, producing a ready environment; 5 test execution, producing results and defect reports; 6 test closure, producing the test summary report. The cycle repeats for every release or sprint." width="880" height="560" loading="lazy"></p>
<p>We will use the same <strong>food delivery app</strong> from Part 2, and follow one new feature through all six phases: <strong>discount codes at checkout</strong>.</p>
<h2>Phase 1: Requirement analysis</h2>
<p><strong>Goal: understand exactly what needs to be tested.</strong></p>
<p>The testers read the requirements and ask questions <em>before</em> any code is written. For discount codes:</p>
<ul>
<li>Can a customer use two codes on one order?</li>
<li>Does the code apply before or after the delivery fee?</li>
<li>What message appears for an expired code?</li>
</ul>
<p>Every unanswered question here is a bug avoided later. Testers also decide which requirements can be tested and how (manual, automated, or both).</p>
<p><strong>Deliverables:</strong> a list of testable requirements, open questions, and often a <strong>Requirements Traceability Matrix (RTM)</strong>: a table that links every requirement to the tests that cover it, so nothing is forgotten.</p>
<h2>Phase 2: Test planning</h2>
<p><strong>Goal: decide how the testing will be done.</strong></p>
<p>A test lead writes the <strong>test plan</strong>, which answers:</p>
<ul>
<li><strong>Scope:</strong> what will be tested, and what won&#39;t. (&quot;Discount codes on web and Android. iPhone next release.&quot;)</li>
<li><strong>Approach:</strong> which test levels and types, and what to automate.</li>
<li><strong>Resources:</strong> who tests what, and with which tools.</li>
<li><strong>Schedule and effort:</strong> how long it will take.</li>
<li><strong>Risks:</strong> what could go wrong, and the backup plan.</li>
<li><strong>Entry and exit criteria</strong> for the whole cycle.</li>
</ul>
<p><strong>Deliverables:</strong> the test plan and effort estimate.</p>
<h2>Phase 3: Test case design</h2>
<p><strong>Goal: write the actual tests.</strong></p>
<p>Testers turn the requirements into <strong>test cases</strong>, each with steps and an expected result, plus the <strong>test data</strong> they need:</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Test case</th>
<th>Expected result</th>
</tr>
</thead>
<tbody><tr>
<td>TC-01</td>
<td>Apply a valid 10% code to a 500 order</td>
<td>Total becomes 450</td>
</tr>
<tr>
<td>TC-02</td>
<td>Apply an expired code</td>
<td>&quot;This code has expired&quot; message, total unchanged</td>
</tr>
<tr>
<td>TC-03</td>
<td>Apply two codes</td>
<td>Second code is refused with a clear message</td>
</tr>
<tr>
<td>TC-04</td>
<td>Apply a code, then empty the cart</td>
<td>Discount is removed</td>
</tr>
</tbody></table>
<p>Test cases are reviewed by another tester or a developer. Automation engineers start writing automated scripts for the cases chosen in planning.</p>
<p><strong>Deliverables:</strong> reviewed test cases, test data and automation scripts. The RTM is updated so every requirement maps to its test cases.</p>
<h2>Phase 4: Test environment setup</h2>
<p><strong>Goal: get a place ready to run the tests.</strong></p>
<p>Tests need somewhere to run that behaves like production: servers, a database with test data, browsers, phones and the right app version. Often a separate team prepares it, and testers do a quick <strong>smoke test</strong> to check it actually works before starting.</p>
<p>This phase often runs <strong>at the same time</strong> as Phase 3.</p>
<p><strong>Deliverables:</strong> a working test environment and smoke test results.</p>
<h2>Phase 5: Test execution</h2>
<p><strong>Goal: run the tests and record what happens.</strong></p>
<p>Testers run the test cases, manual and automated, and mark each one <strong>passed</strong>, <strong>failed</strong>, <strong>blocked</strong> (can&#39;t run, because something else is broken) or <strong>not run</strong>.</p>
<p>When a test fails, they log a <strong>defect</strong> (bug report) with the steps to reproduce it, what was expected, what actually happened, and screenshots or a video. When developers fix it, testers <strong>re-test</strong> that fix, then run <strong>regression tests</strong> to make sure the fix didn&#39;t break anything else.</p>
<p><strong>Deliverables:</strong> test results, defect reports and an updated RTM showing which requirements passed.</p>
<h2>Phase 6: Test closure</h2>
<p><strong>Goal: finish properly and learn for next time.</strong></p>
<p>The team checks the exit criteria. For example: &quot;all critical tests passed, no open critical bugs, at least 95% of planned tests run&quot;. Then they write a <strong>test summary report</strong>:</p>
<ul>
<li>What was tested, and what wasn&#39;t.</li>
<li>How many tests passed, failed and were blocked.</li>
<li>Bugs found, fixed and still open, by severity.</li>
<li>Risks the release still carries.</li>
<li>Lessons learned for the next cycle.</li>
</ul>
<p><strong>Deliverables:</strong> the test summary report, and the test cases and scripts saved for reuse.</p>
<h2>Entry and exit criteria</h2>
<p>Entry and exit criteria stop two classic problems: <strong>starting too early</strong> (testing a build that doesn&#39;t even start) and <strong>never finishing</strong> (testing forever, or stopping on a deadline without knowing the risk).</p>
<table>
<thead>
<tr>
<th>Phase</th>
<th>Example entry criteria</th>
<th>Example exit criteria</th>
</tr>
</thead>
<tbody><tr>
<td>Test case design</td>
<td>Requirements reviewed and signed off</td>
<td>All test cases written and reviewed</td>
</tr>
<tr>
<td>Test execution</td>
<td>Environment ready, smoke test passed</td>
<td>All planned tests run; critical bugs fixed</td>
</tr>
<tr>
<td>Test closure</td>
<td>Execution finished</td>
<td>Summary report shared and signed off</td>
</tr>
</tbody></table>
<h2>How the STLC fits the SDLC</h2>
<p>The <strong>SDLC</strong> (Software Development Life Cycle) covers building the whole product. The STLC is the testing part of it, and it runs <strong>alongside</strong> development, not after it:</p>
<p><img src="https://provalab.io/blog/basics/sdlc-stlc.svg" alt="The SDLC and STLC side by side: requirement analysis runs while requirements are gathered, test planning during design, test design and environment setup while the code is written, test execution once the build is ready, and test closure at release." width="880" height="470" loading="lazy"></p>
<p>This is the &quot;test early&quot; idea from Part 1. When testers start on day one, the test cases are ready the moment the build is.</p>
<h2>STLC in agile teams</h2>
<p>In agile teams, the STLC doesn&#39;t disappear. It gets <strong>smaller and faster</strong>, and repeats every sprint:</p>
<ul>
<li><strong>Requirement analysis</strong> happens during backlog refinement, when testers ask questions about each user story.</li>
<li><strong>Planning</strong> is part of sprint planning, often with an overall test strategy instead of a long plan per sprint.</li>
<li><strong>Design, execution and closure</strong> happen inside the sprint, story by story.</li>
<li><strong>Automation</strong> carries the regression checks from sprint to sprint.</li>
</ul>
<p>Same six steps, just in days instead of months.</p>
<h2>Quick interview recap</h2>
<ol>
<li><strong>STLC</strong> = the six phases of testing: requirement analysis, test planning, test case design, environment setup, test execution, test closure.</li>
<li>Each phase has <strong>entry criteria</strong>, <strong>activities</strong>, <strong>deliverables</strong> and <strong>exit criteria</strong>.</li>
<li>The <strong>RTM</strong> links requirements to test cases, to prove everything is covered.</li>
<li>The STLC runs <strong>in parallel</strong> with the SDLC, and repeats every sprint in agile.</li>
</ol>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/first-automated-test-pytest/">Part 5</a></strong> we get hands-on: <strong>your first automated test in 15 minutes</strong>, with Python and pytest.</p>
<p>ProvaLab&#39;s Test Management follows the same life cycle: requirements linked to test cases, test plans, test runs with every result recorded, and defects linked back to the failing test. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Static testing explained: reviews, walkthroughs, inspections and static analysis</title>
      <link>https://provalab.io/blog/static-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/static-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>static-testing</category>
      <category>code-review</category>
      <description>How to find defects without running the software: the four types of review, the review process, static analysis tools with a real example, and why static testing is the cheapest testing there is.</description>
      <content:encoded><![CDATA[<p>Every test in this series so far has needed one thing: running software. But many defects can be found <strong>before anything runs</strong>, by reading. A requirement that contradicts itself, a design that forgets the error case, a line of code with a typo in a variable name: none of these need a running program to be spotted.</p>
<p>That&#39;s <strong>static testing</strong>, and <a href="https://provalab.io/blog/seven-principles-of-testing/">principle 3</a> explains why it matters so much: the earlier a defect is found, the cheaper it is to fix.</p>
<h2>Static vs dynamic testing</h2>
<p><img src="https://provalab.io/blog/basics/static-vs-dynamic.svg" alt="Static testing examines work products without running the software: requirements, designs, code, test cases and documents, through reviews by people and static analysis by tools. Dynamic testing runs the software and checks its behaviour through unit, integration, system and acceptance tests. Static testing finds the cause directly, from day one; dynamic testing finds failures and needs working software." width="880" height="420" loading="lazy"></p>
<table>
<thead>
<tr>
<th></th>
<th>Static testing</th>
<th>Dynamic testing</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Software runs?</strong></td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td><strong>What is examined</strong></td>
<td>Requirements, designs, code, test cases, user guides</td>
<td>The software&#39;s behaviour</td>
</tr>
<tr>
<td><strong>How</strong></td>
<td>Reviews (people) and static analysis (tools)</td>
<td>Executing tests</td>
</tr>
<tr>
<td><strong>Finds</strong></td>
<td>The defect itself, and its cause</td>
<td>A failure; the cause must then be traced</td>
</tr>
<tr>
<td><strong>Can start</strong></td>
<td>As soon as a document or code exists</td>
<td>Only when runnable software exists</td>
</tr>
<tr>
<td><strong>Typical defects found</strong></td>
<td>Missing requirements, contradictions, design flaws, unreachable code, security weaknesses, standard violations</td>
<td>Wrong behaviour, performance problems, integration failures</td>
</tr>
</tbody></table>
<p>The two find <strong>different</strong> defects. A requirement that was never written can&#39;t be caught by running tests, because no test was written for it either. Only a review notices what&#39;s missing.</p>
<h2>What can be statically tested?</h2>
<p>Almost anything the team writes down:</p>
<ul>
<li><strong>Requirements and user stories:</strong> are they complete, consistent, testable?</li>
<li><strong>Designs and architecture documents</strong></li>
<li><strong>Code</strong></li>
<li><strong>Test plans and test cases</strong> (yes, tests get reviewed too)</li>
<li><strong>User guides, API documentation, configuration files</strong></li>
</ul>
<h2>Reviews: static testing by people</h2>
<p>A <strong>review</strong> is a person, or a group, examining a piece of work to find defects and improve it. Reviews range from a quick &quot;can you look at this?&quot; to a formal procedure with defined roles.</p>
<p><img src="https://provalab.io/blog/basics/review-types.svg" alt="Four types of review on a scale from informal to formal. Informal review: a colleague reads it and comments, no set process. Walkthrough: the author presents, the group discusses, for understanding and consensus. Technical review: experts check the work against standards, with a moderator and recorded findings. Inspection: defined roles, entry and exit criteria, checklists and metrics, for critical work such as payment or safety code." width="880" height="420" loading="lazy"></p>
<h3>Informal review</h3>
<p>A colleague reads the work and comments. No defined process, nothing recorded. Fast and cheap, and far better than no review. Most pair programming and quick desk checks fall here.</p>
<h3>Walkthrough</h3>
<p>The <strong>author leads</strong>: they walk the group through the document or code, explaining it, while the group asks questions and spots problems. The main goals are <strong>shared understanding and learning</strong>, as well as finding defects. Walkthroughs are good for designs and for bringing new team members up to speed.</p>
<h3>Technical review</h3>
<p>A more structured discussion led by a <strong>moderator</strong> rather than the author, with technical experts checking the work against standards, designs and best practice. Findings are <strong>recorded</strong> and followed up. Typical for architecture and API designs.</p>
<h3>Inspection</h3>
<p>The most formal type. Inspections have <strong>defined roles</strong> (moderator, author, reader, reviewers, scribe), <strong>entry criteria</strong> (the work is complete enough to inspect), <strong>checklists</strong>, <strong>metrics</strong> (defects found per page, time spent) and <strong>exit criteria</strong>. They&#39;re expensive, so they&#39;re reserved for critical work: payment logic, safety-related code, legal text.</p>
<h3>Where code review fits</h3>
<p>The <strong>code review</strong> that happens on every pull request is the most common review in modern teams. Depending on the team it sits somewhere between an informal review and a technical review: a reviewer reads the change, comments, and the author fixes issues before the change is merged.</p>
<p>Good code reviews check:</p>
<ul>
<li><strong>Correctness:</strong> does the change do what the ticket says, including error cases?</li>
<li><strong>Tests:</strong> are there tests, and do they test the right things?</li>
<li><strong>Readability:</strong> could someone else maintain this?</li>
<li><strong>Security:</strong> any injection risks, secrets in the code, missing permission checks?</li>
<li><strong>Performance:</strong> anything that will be slow with real data?</li>
</ul>
<h2>The review process</h2>
<p>A formal review follows roughly these steps:</p>
<ol>
<li><strong>Planning:</strong> decide what to review, who reviews it, and the entry criteria.</li>
<li><strong>Kick-off:</strong> share the work and the goals with the reviewers.</li>
<li><strong>Individual review:</strong> each reviewer reads the work <strong>alone first</strong> and notes potential defects. This step is where most defects are found.</li>
<li><strong>Review meeting (or discussion):</strong> reviewers share findings, decide which are real defects, and agree on actions.</li>
<li><strong>Rework:</strong> the author fixes the agreed defects.</li>
<li><strong>Follow-up:</strong> someone checks the fixes were made and the exit criteria are met.</li>
</ol>
<p>Even for a pull request, step 3 matters: a reviewer who reads carefully on their own finds more than a group skimming together.</p>
<h3>A requirements review checklist</h3>
<p>Reviewing requirements is the highest-value static testing there is, because every later step builds on them. For each requirement, ask:</p>
<ul>
<li><strong>Complete:</strong> does it say what happens on errors and edge cases?</li>
<li><strong>Unambiguous:</strong> could two people read it differently? (&quot;Codes give 10% off&quot;: before or after the delivery fee?)</li>
<li><strong>Consistent:</strong> does it contradict another requirement?</li>
<li><strong>Testable:</strong> could you write a test case with a clear pass or fail? (&quot;The app should be fast&quot; is not testable; &quot;search results appear within 1 second&quot; is.)</li>
<li><strong>Necessary and traceable:</strong> does anyone need this, and can it be linked to a business goal?</li>
</ul>
<p>Every question a tester asks here is a bug that will never be written.</p>
<h2>Static analysis: static testing by tools</h2>
<p><strong>Static analysis tools read code without running it</strong> and flag problems automatically. They are built into most modern development setups and run in seconds.</p>
<p>Here is a small, deliberately flawed Python function from our food delivery app:</p>
<pre><code class="language-python">import os
import datetime


def apply_discount(total, code, codes):
    discount = 0
    if code in codes:
        discount = codes[code]
    if discount &gt; 100:
        pass
    final = total - total * discount / 100
    return finall
</code></pre>
<p>Running <strong>Ruff</strong>, a popular Python linter, on it:</p>
<pre><code class="language-text">discount.py:1:8: F401 [*] `os` imported but unused
discount.py:2:8: F401 [*] `datetime` imported but unused
discount.py:11:5: F841 Local variable `final` is assigned to but never used
discount.py:12:12: F821 Undefined name `finall`
Found 5 errors.
</code></pre>
<p>In under a second, without running anything, the tool found:</p>
<ul>
<li>two <strong>unused imports</strong> (clutter, and sometimes a sign of leftover code);</li>
<li>a variable that is <strong>assigned but never used</strong>;</li>
<li>an <strong>undefined name</strong>: <code>finall</code> is a typo for <code>final</code>. This function would crash the first time it ran. Dynamic testing would catch it too, but only after someone wrote and ran a test; static analysis caught it the moment the file was saved.</li>
</ul>
<p>(The fifth finding was about import ordering, a style issue.) Notice what the tool did <strong>not</strong> find: the <code>if discount &gt; 100: pass</code> block does nothing, which a human reviewer would question. Tools and people complement each other.</p>
<h3>What static analysis tools check</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>Examples</th>
<th>Typical tools</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Bugs</strong></td>
<td>Undefined names, unreachable code, null dereferences, resource leaks</td>
<td>Ruff, pyflakes, ESLint, SpotBugs</td>
</tr>
<tr>
<td><strong>Style and formatting</strong></td>
<td>Naming, line length, consistent layout</td>
<td>Black, Prettier, Checkstyle</td>
</tr>
<tr>
<td><strong>Complexity</strong></td>
<td>Functions that are too long or too deeply nested</td>
<td>radon, SonarQube</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td>SQL injection, hard-coded passwords, unsafe functions</td>
<td>Bandit, Semgrep, CodeQL (this is called <strong>SAST</strong>: static application security testing)</td>
</tr>
<tr>
<td><strong>Type errors</strong></td>
<td>Passing a string where a number is expected</td>
<td>mypy, TypeScript</td>
</tr>
<tr>
<td><strong>Dependencies</strong></td>
<td>Known vulnerable library versions</td>
<td>pip-audit, npm audit, Dependabot</td>
</tr>
</tbody></table>
<p>Run them <strong>in the CI pipeline</strong>, so every change is checked automatically before review.</p>
<h2>Why static testing pays off</h2>
<ul>
<li><strong>It&#39;s early.</strong> Requirements can be reviewed before a line of code exists.</li>
<li><strong>It finds the cause, not just the symptom.</strong> A review finds the ambiguous sentence; a failing test only shows that two developers read it differently.</li>
<li><strong>It finds what dynamic testing can&#39;t:</strong> missing requirements, poor design, unreadable code, and security weaknesses that no functional test would trigger.</li>
<li><strong>It&#39;s cheap to repeat.</strong> Static analysis runs on every commit in seconds.</li>
<li><strong>It spreads knowledge.</strong> Reviews teach the team the codebase and each other&#39;s reasoning.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>Skipping requirement reviews</strong> and discovering ambiguity during system testing, when it&#39;s expensive.</li>
<li><strong>Rubber-stamp reviews:</strong> approving a pull request without reading it. A review that finds nothing on a 500-line change probably wasn&#39;t a review.</li>
<li><strong>Reviewing too much at once.</strong> Defect detection drops sharply on huge changes. Smaller changes get better reviews.</li>
<li><strong>Treating linter warnings as noise</strong> and letting hundreds pile up, so the important ones are lost.</li>
<li><strong>Making reviews personal.</strong> Review the work, not the author. The goal is a better product, not a verdict on a person.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/exploratory-testing/">Part 22</a></strong> we look at <strong>exploratory testing</strong>: how skilled testers find the bugs that no script or checklist anticipated.</p>
<p>ProvaLab&#39;s PR Bot performs static testing on every pull request, combining static analysis, security checks and an AI review of the change, and feeds its findings back to the coding agent that wrote the code, so problems are fixed before a human reviewer looks. See it in the <a href="https://provalab.io/#stage-prbot">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>System testing explained: testing the whole product, end to end</title>
      <link>https://provalab.io/blog/system-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/system-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>system-testing</category>
      <description>What system testing is, how it differs from integration and acceptance testing, its functional and non-functional types, example test cases, and how to do it well.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/integration-testing/">Part 12</a> we checked that the pieces of our food delivery app work together, one join at a time. Now every piece is in place. The next question is the one users actually care about: <strong>does the finished product work?</strong></p>
<p>That&#39;s <strong>system testing</strong>, the third of the <a href="https://provalab.io/blog/types-of-software-testing/">four levels of testing</a>.</p>
<h2>What is system testing?</h2>
<p><strong>System testing checks the complete, integrated system against its requirements, from the outside, the way a real user would use it.</strong></p>
<p>Three things make it different from the levels below it:</p>
<ul>
<li><strong>The whole system</strong> is tested, not parts of it: apps, servers, database, notifications and outside services, all running together.</li>
<li><strong>From the outside</strong>, mostly as <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">black-box testing</a>: through the app, the website or the API, without looking at the code.</li>
<li><strong>In a production-like environment</strong>: set up like the live system, with realistic data.</li>
</ul>
<p><img src="https://provalab.io/blog/basics/system-under-test.svg" alt="System testing: the tester works from outside the system, like a user, through the mobile app, website and API, following journeys such as search, order, pay and track. Inside is the complete integrated system (order, payment, restaurant, delivery, database and notifications) running in a production-like test environment with realistic data and a sandbox payment provider." width="880" height="400" loading="lazy"></p>
<h2>System testing vs integration and acceptance testing</h2>
<table>
<thead>
<tr>
<th></th>
<th>Integration testing</th>
<th>System testing</th>
<th>Acceptance testing</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Question</strong></td>
<td>Do the parts work together?</td>
<td>Does the whole system meet its requirements?</td>
<td>Is this what the business and users need?</td>
</tr>
<tr>
<td><strong>Scope</strong></td>
<td>Two or more parts</td>
<td>The complete system</td>
<td>The complete system, in real use</td>
</tr>
<tr>
<td><strong>Done by</strong></td>
<td>Developers, QA</td>
<td>QA team</td>
<td>Customers, product owners, real users</td>
</tr>
<tr>
<td><strong>Based on</strong></td>
<td>Interfaces and design</td>
<td>Requirements and specifications</td>
<td>Business needs and acceptance criteria</td>
</tr>
<tr>
<td><strong>Environment</strong></td>
<td>Test or CI</td>
<td>Production-like test environment</td>
<td>Staging or production-like, often with real users</td>
</tr>
</tbody></table>
<p>A system can pass system testing and still fail acceptance, if it does exactly what the requirements said but the requirements missed what users needed.</p>
<h2>System testing vs end-to-end testing</h2>
<p>The two terms are often used as if they meant the same thing, but there is a small difference:</p>
<ul>
<li><strong>End-to-end (E2E) testing</strong> follows one <strong>complete user journey</strong> through every part of the system.</li>
<li><strong>System testing</strong> is broader: it covers those journeys <strong>plus</strong> non-functional checks like performance, security and compatibility of the whole system.</li>
</ul>
<p>So E2E tests are one important part of system testing.</p>
<h2>Types of system testing</h2>
<p><img src="https://provalab.io/blog/basics/system-test-types.svg" alt="What system testing covers. Functional: complete user journeys, business rules, calculations and data flowing correctly from start to end. Non-functional: performance, security, usability, accessibility, compatibility, recovery, install and upgrade, and localisation." width="880" height="440" loading="lazy"></p>
<h3>Functional system testing</h3>
<p>Does the whole system <strong>do the right thing</strong>?</p>
<ul>
<li><strong>Complete journeys:</strong> sign up, search, order, pay, track and rate.</li>
<li><strong>Business rules:</strong> free delivery over 300, one discount code per order, no orders when the restaurant is closed.</li>
<li><strong>Data end to end:</strong> the order shown to the customer, the restaurant and the delivery partner has the same items and the same total.</li>
</ul>
<h3>Non-functional system testing</h3>
<p>How <strong>well</strong> does the whole system do it?</p>
<ul>
<li><strong>Performance:</strong> does it stay fast with many users at once? (We cover this in Part 16.)</li>
<li><strong>Security:</strong> can users see only their own data? Can someone order without paying?</li>
<li><strong>Usability:</strong> can a first-time user order without help?</li>
<li><strong>Accessibility:</strong> does it work with a screen reader, with only a keyboard, with large text?</li>
<li><strong>Compatibility:</strong> does it work on the browsers, phones and operating systems your users have?</li>
<li><strong>Recovery:</strong> if a server restarts mid-payment, is the order lost, or charged twice?</li>
<li><strong>Installation and upgrade:</strong> does the new app version install cleanly and keep the user&#39;s saved addresses?</li>
<li><strong>Localisation:</strong> are languages, dates, currencies and phone numbers right in every country you serve?</li>
</ul>
<h2>Example system test cases</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Test case</th>
<th>Expected result</th>
</tr>
</thead>
<tbody><tr>
<td>ST-01</td>
<td>New customer signs up, orders 2 pizzas, pays by card</td>
<td>Order confirmed; restaurant and delivery partner see it; confirmation SMS and email arrive</td>
</tr>
<tr>
<td>ST-02</td>
<td>Order 350 worth of food</td>
<td>Delivery fee is 0 (free delivery over 300)</td>
</tr>
<tr>
<td>ST-03</td>
<td>Restaurant closes while customer is at checkout</td>
<td>Customer is told clearly before paying; no charge</td>
</tr>
<tr>
<td>ST-04</td>
<td>Payment provider is down</td>
<td>&quot;Payment failed, please try again&quot;; no order created; no charge</td>
</tr>
<tr>
<td>ST-05</td>
<td>Place an order on Android, view it on the website</td>
<td>Same items, total and status on both</td>
</tr>
<tr>
<td>ST-06</td>
<td>Order in Hindi with the app set to Hindi</td>
<td>All text, dates and prices shown correctly</td>
</tr>
<tr>
<td>ST-07</td>
<td>Navigate the whole checkout with only a keyboard</td>
<td>Every step reachable and usable</td>
</tr>
</tbody></table>
<p>Notice how each one exercises <strong>many parts of the system at once</strong>, and how several test what happens when something goes wrong.</p>
<h2>Entry and exit criteria for system testing</h2>
<p><strong>Start system testing when:</strong></p>
<ul>
<li>Integration testing is complete, with no open critical integration bugs.</li>
<li>The full system is deployed to a production-like environment.</li>
<li>Test data and test accounts are ready.</li>
<li>A smoke test of the build has passed (Part 14).</li>
</ul>
<p><strong>Finish system testing when:</strong></p>
<ul>
<li>All planned system tests have been run.</li>
<li>All high-priority tests pass.</li>
<li>No critical or high-severity bugs are open (or the remaining ones are accepted risks).</li>
<li>The test summary report is shared.</li>
</ul>
<h2>Doing system testing well</h2>
<ol>
<li><strong>Use a production-like environment.</strong> Bugs that only appear with the real configuration, a real-sized database or the real network setup won&#39;t show up on a developer&#39;s laptop.</li>
<li><strong>Use realistic test data.</strong> Long names, addresses with special characters, customers with a hundred past orders. Never use real customers&#39; personal data; create test data that looks like it.</li>
<li><strong>Test the unhappy paths.</strong> Payment failures, closed restaurants, lost network, expired sessions. Users meet these every day.</li>
<li><strong>Think like different users:</strong> a first-time customer, a regular, a restaurant owner, a delivery partner, an admin.</li>
<li><strong>Automate the critical journeys.</strong> Sign-up, sign-in and checkout should run automatically on every release, with tools like Playwright or Selenium. Keep the rest manual or exploratory, as the <a href="https://provalab.io/blog/types-of-software-testing/">testing pyramid</a> suggests.</li>
<li><strong>Trace every test to a requirement</strong>, using the <a href="https://provalab.io/blog/requirements-traceability-matrix/">traceability matrix</a>, so you know the whole specification is covered.</li>
</ol>
<h2>Common mistakes</h2>
<ul>
<li><strong>Testing on a developer environment</strong> that differs from production in small but important ways.</li>
<li><strong>Only testing happy paths</strong> because &quot;the requirements didn&#39;t mention errors&quot;.</li>
<li><strong>Skipping non-functional testing</strong> until after release, then discovering the app is slow or inaccessible.</li>
<li><strong>Too many automated end-to-end tests</strong>, making the suite slow and flaky. Automate the journeys that matter most.</li>
<li><strong>Starting before integration is stable</strong>, so system testers spend their days reporting integration bugs.</li>
</ul>
<h2>What&#39;s next</h2>
<p>Before deep testing like this begins, teams run a quick check to make sure the build is even worth testing. In <strong><a href="https://provalab.io/blog/smoke-vs-sanity-testing/">Part 14</a></strong> we cover <strong>smoke testing and sanity testing</strong>: what each one is, and when to use which.</p>
<p>ProvaLab runs your end-to-end and system tests on real browsers and devices, records a video of every run, and keeps each result linked to its test case and requirement, so system test reports are always up to date. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Test coverage explained: the types, and how to calculate each one</title>
      <link>https://provalab.io/blog/test-coverage-explained/</link>
      <guid isPermaLink="true">https://provalab.io/blog/test-coverage-explained/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-coverage</category>
      <description>The main kinds of test coverage (requirement, execution, code and platform), the formula for each with worked examples, sensible targets, and why 100% coverage isn't bug-free.</description>
      <content:encoded><![CDATA[<p>&quot;What&#39;s our test coverage?&quot; is one of the most common questions in software teams, and one of the most misunderstood. The answer depends on <strong>what</strong> you are measuring the coverage <em>of</em>. A team can honestly say &quot;we have 90% coverage&quot; while a whole feature has never been tested.</p>
<p>In this part we look at the main kinds of test coverage, how to calculate each one, and how to use them without being fooled.</p>
<h2>What is test coverage?</h2>
<p><strong>Test coverage measures how much of something your tests have checked, as a percentage.</strong> That &quot;something&quot; can be:</p>
<ul>
<li>the <strong>requirements</strong> (what the software should do),</li>
<li>the <strong>test cases</strong> you planned to run,</li>
<li>the <strong>code</strong> itself,</li>
<li>the <strong>platforms</strong> your users have: browsers, devices and operating systems.</li>
</ul>
<p>Each kind answers a different question:</p>
<p><img src="https://provalab.io/blog/basics/coverage-types.svg" alt="Four kinds of coverage. Requirement coverage: did we test everything we promised? Requirements with at least one test divided by all requirements, for example 4 of 5, 80%. Execution coverage: did we run the tests we planned? Test cases run divided by test cases planned, for example 113 of 120, 94%. Code coverage: which code did the tests actually run? For example 2 of 4 branches, 50%. Platform coverage: did we test where our users are? Browser and device combinations tested divided by all supported, for example 4 of 6, 67%." width="880" height="470" loading="lazy"></p>
<h2>1. Requirement coverage</h2>
<p><strong>Question:</strong> did we test everything the software is supposed to do?</p>
<blockquote>
<p><strong>Requirement coverage = (requirements with at least one test ÷ all requirements) × 100</strong></p>
</blockquote>
<p><strong>Example:</strong> from the <a href="https://provalab.io/blog/requirements-traceability-matrix/">traceability matrix</a> in Part 11, the discount-code feature has 5 requirements, and 4 of them have test cases (REQ-14, Android, has none).</p>
<p>4 ÷ 5 × 100 = <strong>80%</strong></p>
<p>A stricter version only counts requirements whose tests <strong>pass</strong>:</p>
<blockquote>
<p><strong>Passing requirement coverage = (requirements whose tests all pass ÷ all requirements) × 100</strong></p>
</blockquote>
<p>In the same example, REQ-12 is tested but failing, so: 3 ÷ 5 × 100 = <strong>60%</strong>.</p>
<p>Requirement coverage is the one business people care about most: it speaks the language of features, not code.</p>
<h2>2. Execution coverage</h2>
<p><strong>Question:</strong> did we actually run the tests we planned?</p>
<blockquote>
<p><strong>Execution coverage = (test cases run ÷ test cases planned) × 100</strong></p>
</blockquote>
<p>&quot;Run&quot; means the test produced a real verdict: <strong>passed or failed</strong>. Blocked, skipped and not-run tests don&#39;t count, because we learned nothing from them.</p>
<p><strong>Example:</strong> in Run 42 from <a href="https://provalab.io/blog/test-runs-and-test-results/">Part 17</a>, 120 test cases were planned: 104 passed, 9 failed, 4 were blocked and 3 skipped.</p>
<p>(104 + 9) ÷ 120 × 100 = <strong>94%</strong></p>
<p>Don&#39;t confuse this with the <strong>pass rate</strong>, which asks how many of the tests that ran passed: 104 ÷ 113 = 92%. Execution coverage tells you <em>how much you tested</em>; pass rate tells you <em>how well it did</em>.</p>
<h2>3. Code coverage</h2>
<p><strong>Question:</strong> which parts of the code did the tests actually run?</p>
<p>Code coverage is measured by a tool while the tests run: coverage.py for Python, JaCoCo for Java, Istanbul/nyc or Jest&#39;s built-in coverage for JavaScript, and so on. There are several kinds:</p>
<blockquote>
<p><strong>Line (statement) coverage = (lines executed ÷ all executable lines) × 100</strong></p>
<p><strong>Branch coverage = (branch outcomes taken ÷ all branch outcomes) × 100</strong></p>
<p><strong>Function coverage = (functions called ÷ all functions) × 100</strong></p>
</blockquote>
<p>The difference between line and branch coverage matters a lot. Here is the delivery-fee function from <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Part 7</a>, with a single test:</p>
<p><img src="https://provalab.io/blog/basics/line-vs-branch.svg" alt="The delivery_fee function tested once with a 400 order for a member. All 7 lines run, so line coverage is 7 of 7, 100%. But each of the two if statements can be true or false, and the test only took the true side of both, so branch coverage is 2 of 4, 50%. Small orders and non-members were never tested." width="880" height="400" loading="lazy"></p>
<ul>
<li><strong>Line coverage:</strong> all 7 lines ran → 7 ÷ 7 = <strong>100%</strong>.</li>
<li><strong>Branch coverage:</strong> each <code>if</code> has two outcomes (true and false), so there are 4 outcomes. The test only took the 2 &quot;true&quot; ones → 2 ÷ 4 = <strong>50%</strong>.</li>
</ul>
<p>100% line coverage, yet half the decisions in the code were never tested. That&#39;s why <strong>branch coverage is the more honest number</strong>.</p>
<p><strong>A note on tools:</strong> some tools combine lines and branches into one figure. In Part 7, coverage.py reported <strong>82%</strong> for this same test: it counts the 7 lines plus the 4 branch outcomes, and the test covered 7 + 2 = 9 of those 11. (9 ÷ 11 = 82%.) When you compare coverage numbers, check exactly what each tool is counting.</p>
<p><strong>Path coverage</strong> goes further still: every route through the code. With two <code>if</code> statements there are four paths (big order + member, big + non-member, small + member, small + non-member). Path coverage is very thorough, but grows so fast in real code that it is usually only used for critical logic.</p>
<h2>4. Platform coverage</h2>
<p><strong>Question:</strong> did we test on the browsers and devices our users actually have?</p>
<blockquote>
<p><strong>Platform coverage = (supported platform combinations tested ÷ all supported combinations) × 100</strong></p>
</blockquote>
<p><strong>Example:</strong> the food app officially supports Chrome, Safari and Firefox on desktop, plus the Android app, the iPhone app, and Chrome on Android phones: 6 combinations. This release was tested on 4 of them.</p>
<p>4 ÷ 6 × 100 = <strong>67%</strong></p>
<p>Use your analytics to weight this: missing a browser that 2% of users have matters far less than missing the one that 50% use.</p>
<h2>Other coverage measures you may hear</h2>
<ul>
<li><strong>Risk coverage:</strong> how many of the identified <a href="https://provalab.io/blog/how-to-write-a-test-plan/">high-risk areas</a> have tests.</li>
<li><strong>Feature coverage:</strong> like requirement coverage, but counted per feature or user story.</li>
<li><strong>Mutation score:</strong> a test of your tests. A tool makes small deliberate changes to the code (for example, <code>&gt;=</code> becomes <code>&gt;</code>) and checks whether any test fails. Mutation score = changes caught ÷ changes made × 100. Tests with no real assertions score badly here, even with high code coverage.</li>
</ul>
<h2>What coverage target should you aim for?</h2>
<p>There is no universal right number. Some sensible rules of thumb:</p>
<ul>
<li><strong>Requirement coverage:</strong> aim for <strong>100% of high-priority requirements</strong> before release. Any gap should be a deliberate, documented decision, not a surprise.</li>
<li><strong>Execution coverage:</strong> aim for <strong>100% of planned tests run</strong>, and explain every blocked or skipped one in the test report.</li>
<li><strong>Code coverage:</strong> many teams aim for around 70–80% line or branch coverage, with higher targets for critical code like payments. Chasing 100% everywhere often produces low-value tests written just to touch lines.</li>
<li><strong>Platform coverage:</strong> cover the combinations used by most of your real users, plus anything business-critical.</li>
</ul>
<p>More useful than any single target: <strong>watch the trend</strong>, and make sure coverage doesn&#39;t drop when new code is added.</p>
<h2>Why 100% coverage doesn&#39;t mean bug-free</h2>
<p>Coverage tells you what your tests <strong>touched</strong>. It does not tell you whether they <strong>checked the right things</strong>:</p>
<ul>
<li>A test with no <code>assert</code> still gives coverage.</li>
<li>100% requirement coverage only means each requirement has <em>a</em> test, not a good one.</li>
<li>Code coverage can&#39;t find a feature that was <strong>never built</strong>. You can&#39;t cover code that doesn&#39;t exist (see <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">Part 7</a>).</li>
<li><a href="https://provalab.io/blog/how-to-write-test-cases/">You can&#39;t test every possible input</a>, so coverage will always be of a chosen sample.</li>
</ul>
<p>So use coverage as a <strong>map of what you haven&#39;t tested</strong>, not as proof of quality. Low coverage is a clear warning; high coverage is a good sign, not a guarantee.</p>
<h2>Quick reference</h2>
<table>
<thead>
<tr>
<th>Coverage</th>
<th>Formula</th>
<th>Answers</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Requirement</strong></td>
<td>requirements with tests ÷ all requirements</td>
<td>Did we test everything we promised?</td>
</tr>
<tr>
<td><strong>Execution</strong></td>
<td>tests run ÷ tests planned</td>
<td>Did we run what we planned?</td>
</tr>
<tr>
<td><strong>Line / statement</strong></td>
<td>lines run ÷ all lines</td>
<td>Which code was touched?</td>
</tr>
<tr>
<td><strong>Branch</strong></td>
<td>branch outcomes taken ÷ all outcomes</td>
<td>Did every decision go both ways?</td>
</tr>
<tr>
<td><strong>Function</strong></td>
<td>functions called ÷ all functions</td>
<td>Which functions were used?</td>
</tr>
<tr>
<td><strong>Platform</strong></td>
<td>combinations tested ÷ combinations supported</td>
<td>Did we test where our users are?</td>
</tr>
</tbody></table>
<h2>What&#39;s next</h2>
<p>That completes this batch of the Testing Basics series, from <a href="https://provalab.io/blog/what-is-software-testing/">what testing is</a> to coverage. Coming up next: <strong><a href="https://provalab.io/blog/seven-principles-of-testing/">the 7 principles of testing</a></strong>, <strong>QA vs QC</strong>, <strong>static testing</strong>, <strong>exploratory testing</strong> and <strong>testing in agile and CI/CD</strong>.</p>
<p>ProvaLab Test Management keeps requirement and execution coverage up to date automatically: requirements link to test cases, every manual or automated run records its results, and coverage reports show the gaps by feature, test plan and run. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Test runs and test results explained: statuses, pass rates and how to read them</title>
      <link>https://provalab.io/blog/test-runs-and-test-results/</link>
      <guid isPermaLink="true">https://provalab.io/blog/test-runs-and-test-results/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>test-execution</category>
      <description>What a test run and a test result are, how they relate to test cases, every result status, how to calculate pass rates correctly, and how to read a run so nothing important is missed.</description>
      <content:encoded><![CDATA[<p>So far in this series you have <a href="https://provalab.io/blog/how-to-write-test-cases/">written test cases</a>, <a href="https://provalab.io/blog/how-to-write-a-test-plan/">planned the testing</a> and learned about <a href="https://provalab.io/blog/types-of-software-testing/">the kinds of testing</a>. But a test case on its own doesn&#39;t tell you anything about the software. Only when it is <strong>run</strong> do you learn something.</p>
<p>This part explains <strong>test runs</strong> and <strong>test results</strong>: what they are, what each result status means, how to calculate a pass rate correctly, and how to read a run like an experienced tester.</p>
<h2>Test case, test run and test result</h2>
<p>These three are easy to confuse, but each has a different job:</p>
<ul>
<li><strong>Test case:</strong> <em>what</em> to check. Written once and kept in the catalogue. &quot;TC-104: an expired discount code must be refused.&quot;</li>
<li><strong>Test run:</strong> <em>one execution</em> of a set of test cases, at a specific time, on a specific <strong>build</strong> and <strong>environment</strong>. &quot;Run 42: release 2.4 regression, build 2.4.0, staging, 4 November.&quot;</li>
<li><strong>Test result:</strong> <em>what happened</em> to <strong>one test case in one run</strong>. &quot;TC-104 in Run 42: Failed. The total became 450 instead of showing &#39;code expired&#39;.&quot;</li>
</ul>
<p><img src="https://provalab.io/blog/basics/case-run-result.svg" alt="Test cases TC-101, TC-104 and TC-105 are written once in the catalogue. Run 41, on build 2.3.9 on 28 October, recorded three passed results. Run 42, on build 2.4.0 on 4 November, recorded TC-101 passed, TC-104 failed with video and logs attached and BUG-231 raised, and TC-105 passed. One test case across many runs builds a history of results." width="880" height="470" loading="lazy"></p>
<p><strong>A test case is written once. It is run many times. Every run gives it a new result.</strong> Over months, those results become the test case&#39;s <strong>history</strong>: &quot;TC-104 passed on every build until 2.4.0&quot;.</p>
<p>You may also hear <strong>test execution</strong> (the act of running tests) and <strong>test cycle</strong> (one round of testing, often made up of several runs, such as &quot;the release 2.4 test cycle&quot;).</p>
<h2>What a test run records</h2>
<table>
<thead>
<tr>
<th>Field</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Name</strong></td>
<td>Release 2.4 regression</td>
</tr>
<tr>
<td><strong>Test plan</strong></td>
<td>Release 2.4: discount codes</td>
</tr>
<tr>
<td><strong>Build / version</strong></td>
<td>2.4.0</td>
</tr>
<tr>
<td><strong>Environment</strong></td>
<td>Staging, Chrome 131 and Android 14</td>
</tr>
<tr>
<td><strong>Test cases included</strong></td>
<td>120 from the regression suite</td>
</tr>
<tr>
<td><strong>Run by</strong></td>
<td>A tester, or automated (nightly pipeline)</td>
</tr>
<tr>
<td><strong>Started / finished</strong></td>
<td>4 Nov 02:00 – 02:47</td>
</tr>
<tr>
<td><strong>Status</strong></td>
<td>Completed</td>
</tr>
</tbody></table>
<p>The <strong>build</strong> and <strong>environment</strong> are what make a run meaningful. &quot;TC-104 failed&quot; is not useful on its own; &quot;TC-104 failed on build 2.4.0 on staging, and passed on 2.3.9&quot; tells a developer exactly where to look.</p>
<h2>What a test result records</h2>
<table>
<thead>
<tr>
<th>Field</th>
<th>Example</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Status</strong></td>
<td>Failed</td>
</tr>
<tr>
<td><strong>Actual result</strong></td>
<td>Code OLD10 accepted; total became 450</td>
</tr>
<tr>
<td><strong>Error message</strong></td>
<td><code>AssertionError: expected 500, got 450</code> (for automated tests)</td>
</tr>
<tr>
<td><strong>Duration</strong></td>
<td>8.2 seconds</td>
</tr>
<tr>
<td><strong>Evidence</strong></td>
<td>Screenshot, screen recording, console and network logs</td>
</tr>
<tr>
<td><strong>Linked bug</strong></td>
<td>BUG-231</td>
</tr>
<tr>
<td><strong>Comment</strong></td>
<td>Fails on web and Android; passed on 2.3.9</td>
</tr>
</tbody></table>
<p>For a passed test, the status is often enough. For a failure, the evidence is what turns a red mark into something a developer can fix.</p>
<h2>Every result status explained</h2>
<table>
<thead>
<tr>
<th>Status</th>
<th>Meaning</th>
<th>What to do</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Passed</strong></td>
<td>The actual result matched the expected result</td>
<td>Nothing; its history grows</td>
</tr>
<tr>
<td><strong>Failed</strong></td>
<td>The actual result was different</td>
<td>Investigate; raise or link a bug</td>
</tr>
<tr>
<td><strong>Blocked</strong></td>
<td>The test couldn&#39;t run because something else is broken</td>
<td>Fix or report the blocker, then run it</td>
</tr>
<tr>
<td><strong>Skipped</strong></td>
<td>Deliberately not run this time</td>
<td>Make sure the reason is known and acceptable</td>
</tr>
<tr>
<td><strong>Not run / Untested</strong></td>
<td>Planned, but not run yet</td>
<td>Run it, or explain why not</td>
</tr>
<tr>
<td><strong>In progress</strong></td>
<td>Being run right now</td>
<td>Wait for the result</td>
</tr>
</tbody></table>
<p><strong>Blocked is not the same as failed.</strong> If sign-in is broken, every test that needs a signed-in user is <em>blocked</em>: we learned nothing about those features, good or bad. Counting them as failed exaggerates the damage; ignoring them hides how much is untested.</p>
<h2>Calculating the pass rate correctly</h2>
<p>Here is the summary of Run 42:</p>
<p><img src="https://provalab.io/blog/basics/run-summary.svg" alt="Run 42, the release 2.4 regression on build 2.4.0, with 120 test cases planned: 104 passed, 9 failed, 4 blocked, 3 skipped. Pass rate of executed tests: 104 divided by 113, 92 percent. Pass rate of planned tests: 104 divided by 120, 87 percent. The 9 failures grouped by cause: 6 from a renamed checkout button with outdated test selectors, 2 from BUG-231, and 1 timeout in the payment sandbox." width="880" height="420" loading="lazy"></p>
<p>There are two different pass rates, and both are useful:</p>
<ul>
<li><strong>Pass rate of executed tests</strong> = passed ÷ (passed + failed)
= 104 ÷ 113 = <strong>92%</strong>. <em>How good is the software in the parts we actually tested?</em></li>
<li><strong>Pass rate of planned tests</strong> = passed ÷ all planned tests
= 104 ÷ 120 = <strong>87%</strong>. <em>How much of the plan has been proven to work?</em></li>
</ul>
<p>The gap between them is the <strong>blocked</strong> and <strong>skipped</strong> tests: 7 tests, nearly 6% of the plan, that told us nothing. A report that only says &quot;92% passed&quot; hides that.</p>
<h2>How to read a test run</h2>
<p>A run with 9 red results is not 9 problems. Read it like this:</p>
<ol>
<li><strong>Check the run is valid.</strong> Right build? Right environment? Did it finish, or stop halfway?</li>
<li><strong>Look at blocked and skipped first.</strong> They show what you <em>didn&#39;t</em> learn.</li>
<li><strong>Group failures by cause, not by test.</strong> In Run 42, six failures come from <strong>one</strong> cause: a renamed button that the tests still look for by its old name. That&#39;s one fix to the test code, not six bugs.</li>
<li><strong>Separate real bugs from test or environment problems.</strong><ul>
<li><strong>Product bug:</strong> the app is wrong (BUG-231).</li>
<li><strong>Test problem:</strong> the test is outdated or wrong (the renamed button).</li>
<li><strong>Environment problem:</strong> something around the app failed (the payment sandbox timeout).</li>
</ul>
</li>
<li><strong>Compare with the previous run.</strong> A test that passed yesterday and fails today on a new build is a likely <strong>regression</strong> (see <a href="https://provalab.io/blog/regression-testing-vs-retesting/">Part 15</a>). A test that fails on some runs and passes on others with no change is probably <strong>flaky</strong>.</li>
<li><strong>Read the actual error before describing a failure.</strong> Don&#39;t guess from a test&#39;s name what went wrong; open the result, its message and its recording.</li>
<li><strong>Link every real failure to a bug</strong> (see <a href="https://provalab.io/blog/how-to-write-a-bug-report/">Part 9</a>), so the run, the test case and the bug can all be traced to each other.</li>
</ol>
<h2>Manual and automated runs</h2>
<ul>
<li><strong>Manual runs:</strong> a tester works through the test cases and records each result by hand, with screenshots for failures.</li>
<li><strong>Automated runs:</strong> a tool runs the tests (often every night, or on every code change) and records the results itself, often with logs and video.</li>
</ul>
<p>Most teams have both, and the best practice is to <strong>keep their results in the same place</strong>, so a release report covers everything tested, however it was run.</p>
<h2>Common mistakes</h2>
<ul>
<li><strong>Only reporting &quot;X% passed&quot;</strong>, without the blocked and skipped tests.</li>
<li><strong>Re-running failed tests until they pass</strong>, then reporting green. That hides flaky tests and real intermittent bugs.</li>
<li><strong>Counting failures instead of causes</strong>, so one broken selector looks like a disaster.</li>
<li><strong>Not recording the build and environment</strong>, so nobody can tell what the result applies to.</li>
<li><strong>Results scattered across spreadsheets, chat messages and CI logs</strong>, so nobody has the full picture.</li>
</ul>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/test-coverage-explained/">Part 18</a></strong>, the last of this set, we look at <strong>test coverage</strong>: the different kinds, and how to calculate each one.</p>
<p>In ProvaLab Test Management, every run, manual or automated, records its build, environment and a result for each test case, with the error, screen recording and logs attached to failures. Each test case keeps its full history across runs, and failures can be grouped by cause and linked to bugs. See it in the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Testing in agile and CI/CD: how it all fits together</title>
      <link>https://provalab.io/blog/testing-in-agile-and-ci-cd/</link>
      <guid isPermaLink="true">https://provalab.io/blog/testing-in-agile-and-ci-cd/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>agile-testing</category>
      <category>ci-cd</category>
      <description>How testing works when a team ships every week or every day: whole-team quality, the agile testing quadrants, shift-left and shift-right, TDD and BDD, and testing at every stage of a CI/CD pipeline.</description>
      <content:encoded><![CDATA[<p>Most of the ideas in this series are decades old. What has changed is <strong>speed</strong>. Teams that used to release twice a year now release every week, every day, or many times a day. There is no longer a &quot;testing phase&quot; at the end, because there is no end: the next change is always on its way.</p>
<p>This final part of Testing Basics shows how everything we&#39;ve covered fits together when a team works like that.</p>
<h2>Testing in agile teams</h2>
<p>Agile methods such as Scrum and Kanban build software in short cycles (<strong>sprints</strong> of one to four weeks, or a continuous flow of work). Four things change for testing:</p>
<h3>1. Quality is the whole team&#39;s job</h3>
<p>There&#39;s no separate testing phase, so there can&#39;t be a separate testing team that gets the software at the end. Developers write <a href="https://provalab.io/blog/first-automated-test-pytest/">unit tests</a> and review each other&#39;s code. Testers join the planning conversations, ask the awkward questions early, and spend their time on the things only people can do: <a href="https://provalab.io/blog/exploratory-testing/">exploratory testing</a>, test design and risk. This is often called the <strong>whole-team approach</strong>.</p>
<h3>2. Testing happens inside the sprint</h3>
<p>Each user story is tested in the same sprint it&#39;s built. A story isn&#39;t &quot;done&quot; when the code is written; it&#39;s done when it meets the team&#39;s <strong>definition of done</strong>, which usually includes: tests written and passing, code reviewed, acceptance criteria checked, and no open bugs of high severity.</p>
<h3>3. Acceptance criteria are the test cases</h3>
<p>Each user story carries <strong>acceptance criteria</strong> agreed before development starts. They play the role that requirements and test cases played in the <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">STLC</a>: they say what &quot;working&quot; means, and each one becomes at least one test. A common format is <strong>Given / When / Then</strong>:</p>
<blockquote>
<p><strong>Given</strong> a signed-in customer with 500 in the cart
<strong>When</strong> they apply the expired code OLD10
<strong>Then</strong> they see &quot;This code has expired&quot; and the total stays 500</p>
</blockquote>
<h3>4. Automation is not optional</h3>
<p>If the regression suite takes a week to run by hand, you can&#39;t release every week. Agile teams <strong>automate</strong> their <a href="https://provalab.io/blog/regression-testing-vs-retesting/">regression tests</a> and run them continuously, so the humans are free to explore.</p>
<h2>The agile testing quadrants</h2>
<p>Brian Marick&#39;s <strong>agile testing quadrants</strong>, made popular by Lisa Crispin and Janet Gregory, are a map of all the testing an agile team does. They answer the question &quot;are we doing all the kinds of testing we need, or only the easy ones?&quot;</p>
<p><img src="https://provalab.io/blog/basics/agile-quadrants.svg" alt="The agile testing quadrants. Q1, technology-facing and supporting the team: unit, component and integration tests, static analysis and code review, automated. Q2, business-facing and supporting the team: functional and story tests, examples and acceptance criteria, prototypes, automated and manual. Q3, business-facing and critiquing the product: exploratory testing, usability and user acceptance testing, alpha and beta releases, manual. Q4, technology-facing and critiquing the product: performance, load, security, compatibility and recovery tests, tool-based." width="880" height="520" loading="lazy"></p>
<ul>
<li><strong>Q1 and Q2 support the team while building:</strong> they tell developers quickly whether the code does what the story asked.</li>
<li><strong>Q3 and Q4 critique the finished product:</strong> they find what the stories didn&#39;t anticipate.</li>
</ul>
<p>Most teams are strong in Q1 and Q2 and weak in Q3 and Q4. If your team has thousands of unit tests but nobody has done a <a href="https://provalab.io/blog/performance-testing-types/">load test</a> or an exploratory session this quarter, the quadrants show the gap.</p>
<h2>Shift left, shift right</h2>
<p><strong>Shift left</strong> means moving testing <strong>earlier</strong> in the process: reviewing requirements, writing tests before code, running tests on every commit. Everything from <a href="https://provalab.io/blog/seven-principles-of-testing/">principle 3</a> (early testing) and <a href="https://provalab.io/blog/static-testing/">static testing</a> is shifting left.</p>
<p><strong>Shift right</strong> means continuing to test <strong>after release</strong>, in production: monitoring, alerts, feature flags that release a change to 1% of users first, and small smoke checks that run against the live site. Shift right accepts that some problems only show up with real users and real data, and makes sure you learn about them in minutes rather than weeks.</p>
<p>Modern teams do both: catch what you can early, and watch closely for what you couldn&#39;t.</p>
<h2>TDD and BDD</h2>
<p>Two practices shape how tests are written in agile teams:</p>
<p><strong>Test-driven development (TDD)</strong> is a developer practice: write a failing unit test <em>first</em>, write just enough code to make it pass, then clean up, and repeat. The tests become a design tool, and every line of code exists because a test asked for it. (You saw the value of a test you&#39;ve watched fail in <a href="https://provalab.io/blog/first-automated-test-pytest/">Part 5</a>.)</p>
<p><strong>Behaviour-driven development (BDD)</strong> applies the same idea at the story level, using the Given/When/Then format above. The acceptance criteria are written so that tools such as Cucumber, Behave or SpecFlow can run them directly as automated tests. The main benefit is shared language: product owners, developers and testers all read the same examples.</p>
<p>Neither is mandatory for agile, but both make &quot;acceptance criteria are the test cases&quot; literally true.</p>
<h2>Continuous integration and continuous delivery</h2>
<p><strong>Continuous integration (CI)</strong> means every change is merged into the shared codebase frequently (at least daily) and <strong>automatically built and tested</strong> each time. <strong>Continuous delivery (CD)</strong> extends the pipeline so that every change that passes the tests is ready to release, and <strong>continuous deployment</strong> releases it automatically.</p>
<p>The pipeline is where all the test types in this series take their places:</p>
<p><img src="https://provalab.io/blog/basics/ci-cd-pipeline.svg" alt="A seven-stage pipeline. Commit: static analysis and unit tests, about a minute. Pull request: code review, integration and API tests, security scan, about ten minutes. Merge to main: build, deploy to a test environment, smoke test. Nightly: full regression on all browsers and performance tests, hours. Release candidate: system, exploratory and acceptance testing on staging, days. Production: a smoke test on production and monitoring. Each stage is a gate; testing is fast on the left and deep on the right." width="880" height="440" loading="lazy"></p>
<table>
<thead>
<tr>
<th>Stage</th>
<th>What runs</th>
<th>How long</th>
<th>What it catches</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Every commit</strong></td>
<td><a href="https://provalab.io/blog/static-testing/">Static analysis</a>, unit tests</td>
<td>About a minute</td>
<td>Typos, logic errors, broken units</td>
</tr>
<tr>
<td><strong>Every pull request</strong></td>
<td>Code review, <a href="https://provalab.io/blog/integration-testing/">integration</a> and API tests, security scan</td>
<td>About 10 minutes</td>
<td>Broken joins, insecure code</td>
</tr>
<tr>
<td><strong>Merge to main</strong></td>
<td>Build, deploy to test environment, <a href="https://provalab.io/blog/smoke-vs-sanity-testing/">smoke test</a></td>
<td>About 15 minutes</td>
<td>A build that doesn&#39;t even start</td>
</tr>
<tr>
<td><strong>Nightly</strong></td>
<td>Full <a href="https://provalab.io/blog/regression-testing-vs-retesting/">regression</a> on every browser, <a href="https://provalab.io/blog/performance-testing-types/">performance tests</a></td>
<td>Hours</td>
<td>Regressions, slowdowns</td>
</tr>
<tr>
<td><strong>Release candidate</strong></td>
<td><a href="https://provalab.io/blog/system-testing/">System</a>, <a href="https://provalab.io/blog/exploratory-testing/">exploratory</a> and acceptance testing on staging</td>
<td>Days</td>
<td>What the scripts didn&#39;t think of</td>
</tr>
<tr>
<td><strong>Production</strong></td>
<td>Smoke test on the live site, monitoring and alerts</td>
<td>Minutes, then continuous</td>
<td>Problems only real traffic reveals</td>
</tr>
</tbody></table>
<p>Three rules make a pipeline work:</p>
<ol>
<li><strong>Each stage is a gate.</strong> A failure stops the change from going further, and the team fixes it before doing anything else. A red pipeline that everyone ignores protects nobody.</li>
<li><strong>Fast first, deep later.</strong> The tests that run on every commit must be fast, or developers stop committing often. The slow, thorough tests run less frequently.</li>
<li><strong>Every result is recorded.</strong> Which build, which environment, which tests, which result (see <a href="https://provalab.io/blog/test-runs-and-test-results/">test runs</a>), so a failure can be traced and a release can be reported on.</li>
</ol>
<h3>What a pipeline looks like</h3>
<p>Here is a minimal GitHub Actions workflow that runs the static analysis and unit tests from Parts 5 and 21 on every push:</p>
<pre><code class="language-yaml">name: tests
on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: &quot;3.12&quot;
      - run: pip install pytest ruff
      - run: ruff check .        # static analysis
      - run: pytest              # unit tests
</code></pre>
<p>If either command fails, the workflow fails, the pull request shows a red cross, and the change can&#39;t be merged until it&#39;s fixed. Real pipelines add more stages, but they all start like this.</p>
<h2>Where AI coding agents fit</h2>
<p>The newest change to this picture is that much of the code is now written by AI coding agents such as Claude Code and Cursor. That makes two things in this series more important, not less:</p>
<ul>
<li><strong>Shift left matters more</strong>, because code is produced faster than people can review it. Static analysis, unit tests and automated review on every pull request are what keep up.</li>
<li><strong>The pipeline becomes the agent&#39;s feedback loop.</strong> When a test fails, the agent that wrote the code can read the failure, the logs and the recording, fix the code and run the tests again, with a person approving the result rather than doing the checking by hand.</li>
</ul>
<p>That is the idea ProvaLab is built around, and the subject of our first post, <a href="https://provalab.io/blog/how-to-test-code-your-ai-agent-wrote/">how to test code your AI coding agent wrote</a>.</p>
<h2>Common mistakes</h2>
<ul>
<li><strong>Keeping a separate &quot;testing sprint&quot; at the end.</strong> That&#39;s a waterfall with shorter phases.</li>
<li><strong>Ignoring red pipelines.</strong> Once one failure is tolerated, every failure is.</li>
<li><strong>A slow pipeline.</strong> If the commit stage takes 30 minutes, people batch up changes, and the fast feedback is lost.</li>
<li><strong>Only Q1 and Q2 testing.</strong> Unit tests and story tests don&#39;t tell you the product is slow, insecure or confusing.</li>
<li><strong>No shift right.</strong> Releasing without monitoring means customers are your alerting system.</li>
</ul>
<h2>Testing Basics: the complete series</h2>
<p>That&#39;s the end of Testing Basics. Over 23 parts you&#39;ve learned:</p>
<p><strong>Foundations:</strong> <a href="https://provalab.io/blog/what-is-software-testing/">what testing is</a> · <a href="https://provalab.io/blog/types-of-software-testing/">types and levels</a> · <a href="https://provalab.io/blog/manual-vs-automated-testing/">manual vs automated</a> · <a href="https://provalab.io/blog/software-testing-life-cycle-stlc/">the STLC</a> · <a href="https://provalab.io/blog/seven-principles-of-testing/">the 7 principles</a> · <a href="https://provalab.io/blog/qa-vs-qc-verification-vs-validation/">QA vs QC, verification vs validation</a></p>
<p><strong>The craft:</strong> <a href="https://provalab.io/blog/first-automated-test-pytest/">your first automated test</a> · <a href="https://provalab.io/blog/how-to-write-test-cases/">writing test cases</a> · <a href="https://provalab.io/blog/black-box-white-box-grey-box-testing/">test design techniques</a> · <a href="https://provalab.io/blog/how-to-write-a-test-plan/">test plans</a> · <a href="https://provalab.io/blog/how-to-write-a-bug-report/">bug reports</a> · <a href="https://provalab.io/blog/bug-life-cycle/">the bug life cycle</a> · <a href="https://provalab.io/blog/requirements-traceability-matrix/">the traceability matrix</a> · <a href="https://provalab.io/blog/test-runs-and-test-results/">test runs and results</a> · <a href="https://provalab.io/blog/test-coverage-explained/">test coverage</a></p>
<p><strong>The test types:</strong> <a href="https://provalab.io/blog/integration-testing/">integration</a> · <a href="https://provalab.io/blog/system-testing/">system</a> · <a href="https://provalab.io/blog/smoke-vs-sanity-testing/">smoke and sanity</a> · <a href="https://provalab.io/blog/regression-testing-vs-retesting/">regression and re-testing</a> · <a href="https://provalab.io/blog/performance-testing-types/">performance</a> · <a href="https://provalab.io/blog/static-testing/">static testing</a> · <a href="https://provalab.io/blog/exploratory-testing/">exploratory testing</a> · agile and CI/CD (this post)</p>
<p>Next on the blog, the <strong>Hands-On Testing</strong> series: deeper guides to UI testing, API testing, security testing and load testing, each with examples you can run. It starts with <strong><a href="https://provalab.io/blog/ui-testing-explained/">Part 1: UI testing explained</a></strong>.</p>
<p>ProvaLab is built for the pipeline described here: the PR Bot checks every pull request, QA Infrastructure runs the tests on real browsers and devices, Test Management records every run, and AppSec and load testing cover Q4, with your coding agent able to drive all of it. See it in the <a href="https://provalab.io/#stage-prbot">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Types of software testing explained: unit, integration, system and acceptance</title>
      <link>https://provalab.io/blog/types-of-software-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/types-of-software-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>software-testing</category>
      <description>The four levels of software testing in plain language, with one running example, plus the testing pyramid and the other testing words you will hear at work.</description>
      <content:encoded><![CDATA[<p>In <a href="https://provalab.io/blog/what-is-software-testing/">Part 1</a> we saw what software testing is and why every team needs it. Now the obvious next question: <em>what exactly do you test, and in what order?</em></p>
<p>Testers answer that with <strong>four levels of testing</strong>. Each level looks at the software from a different distance, from one small piece of code up to the whole product in a customer&#39;s hands. We will walk through all four using one example: a <strong>food delivery app</strong>.</p>
<h2>The four levels at a glance</h2>
<p><img src="https://provalab.io/blog/basics/test-levels.svg" alt="The four levels of testing on a food delivery app. Unit: the function that adds up the order total. Integration: the order service saves the order and tells the restaurant. System: search, add to cart, pay and track the delivery. Acceptance: a restaurant owner confirms orders arrive the way they need." width="880" height="540" loading="lazy"></p>
<h2>1. Unit testing</h2>
<p><strong>A unit test checks one small piece of code on its own</strong>, usually a single function.</p>
<p>In our app, a unit test might check the function that adds up an order:</p>
<ul>
<li>Two pizzas at 200 and a drink at 50 should total <strong>450</strong>.</li>
<li>A 10% discount code should bring it to <strong>405</strong>.</li>
<li>An empty cart should total <strong>0</strong>, not crash.</li>
</ul>
<p>Developers write unit tests while they write the code. They run in milliseconds, so a project can have thousands of them and still check everything on every change.</p>
<p><strong>What it can&#39;t tell you:</strong> whether the pieces work <em>together</em>. A perfect total function is no use if the checkout page never calls it.</p>
<h2>2. Integration testing</h2>
<p><strong>An integration test checks that pieces work together</strong>: your code with the database, with another service, or with an outside system like a payment provider.</p>
<p>In our app: when a customer places an order, does the order service <strong>save it to the database</strong> and <strong>notify the restaurant</strong>? Each piece may pass its own unit tests and still fail here. A field could be named differently on each side, or a date could be sent in the wrong format.</p>
<p>Integration tests are slower than unit tests, because they need real databases or services (or realistic stand-ins). Teams usually have fewer of them.</p>
<h2>3. System testing</h2>
<p><strong>A system test checks the whole app, end to end, the way a user would use it.</strong></p>
<p>In our app: open the app, search for &quot;pizza&quot;, add two to the cart, pay, and follow the delivery on the map. Every part of the system takes part: the screens, the servers, the database and the payment provider.</p>
<p>System testing also covers the non-functional questions from Part 1: is it fast enough, is it secure, is it easy to use? This is usually where QA engineers and automated UI tests do most of their work. You will often hear these called <strong>end-to-end (E2E) tests</strong>.</p>
<h2>4. Acceptance testing</h2>
<p><strong>An acceptance test checks that the software actually solves the business need</strong>, and is ready to release.</p>
<p>The question changes from &quot;does it work?&quot; to &quot;is this what we needed?&quot; In our app, a restaurant owner might try the new order screen and confirm that orders arrive with everything their kitchen needs. If the app works perfectly but shows orders in a way the kitchen can&#39;t use, it fails acceptance.</p>
<p>Acceptance testing is often done by customers, product managers or a small group of real users. You will hear names like <strong>UAT</strong> (user acceptance testing) and <strong>beta testing</strong> (a release to a small group of real users first).</p>
<h2>The testing pyramid: how many of each?</h2>
<p>If end-to-end tests are closest to what users do, why not write only those? Because they are slow, costly to maintain, and when one fails it can take a while to find out why.</p>
<p>The <strong>testing pyramid</strong>, an idea popularised by Mike Cohn, gives a simple rule of thumb:</p>
<p><img src="https://provalab.io/blog/basics/test-pyramid.svg" alt="The testing pyramid: many unit tests at the bottom, which are fast and cheap; some integration tests in the middle; and a few end-to-end tests at the top, which are slow and costly but closest to what users do." width="880" height="470" loading="lazy"></p>
<ul>
<li><strong>Many unit tests</strong> at the base: fast, cheap, and they point straight at the broken line.</li>
<li><strong>Some integration tests</strong> in the middle.</li>
<li><strong>A few end-to-end tests</strong> at the top, for the journeys that matter most, such as sign-up and checkout.</li>
</ul>
<p>The opposite shape, lots of slow end-to-end tests and few unit tests, is sometimes called the <strong>ice-cream cone</strong>. Its test runs are slow, flaky and hard to debug.</p>
<h2>Other testing words you will hear</h2>
<p>Not every testing word is a level. Some describe <em>why</em> or <em>when</em> you run tests, at any level:</p>
<table>
<thead>
<tr>
<th>Term</th>
<th>What it means</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Smoke test</strong></td>
<td>A quick check that the most basic things work, such as &quot;the app starts and the home page loads&quot;, before deeper testing</td>
</tr>
<tr>
<td><strong>Sanity test</strong></td>
<td>A quick, narrow check that one specific fix or change works</td>
</tr>
<tr>
<td><strong>Regression test</strong></td>
<td>Re-running existing tests to make sure a change didn&#39;t break something that used to work</td>
</tr>
<tr>
<td><strong>End-to-end (E2E) test</strong></td>
<td>A test of a full user journey through the whole system</td>
</tr>
<tr>
<td><strong>Black-box testing</strong></td>
<td>Testing without looking at the code, only at inputs and outputs</td>
</tr>
<tr>
<td><strong>White-box testing</strong></td>
<td>Testing with knowledge of the code, to cover its paths and branches</td>
</tr>
</tbody></table>
<h2>Putting it together</h2>
<table>
<thead>
<tr>
<th>Level</th>
<th>Checks</th>
<th>Who usually</th>
<th>Speed</th>
<th>How many</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Unit</strong></td>
<td>One piece of code</td>
<td>Developers</td>
<td>Milliseconds</td>
<td>Many</td>
</tr>
<tr>
<td><strong>Integration</strong></td>
<td>Pieces working together</td>
<td>Developers, QA</td>
<td>Seconds</td>
<td>Some</td>
</tr>
<tr>
<td><strong>System (E2E)</strong></td>
<td>The whole app like a user</td>
<td>QA, automation</td>
<td>Minutes</td>
<td>Few</td>
</tr>
<tr>
<td><strong>Acceptance</strong></td>
<td>The real business need</td>
<td>Customers, product</td>
<td>Days</td>
<td>A handful</td>
</tr>
</tbody></table>
<p>A healthy project has all four. Each level catches problems the others miss.</p>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/manual-vs-automated-testing/">Part 3</a></strong> we look at <strong>manual vs automated testing</strong>: what to automate, what not to, and why.</p>
<p>Running end-to-end tests is where ProvaLab helps most: your coding agent runs them on real browsers and devices, every result is kept in one place, and failures come back with a video and logs. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>UI testing explained: what it is, how it works, and Selenium vs Playwright vs Cypress</title>
      <link>https://provalab.io/blog/ui-testing-explained/</link>
      <guid isPermaLink="true">https://provalab.io/blog/ui-testing-explained/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>hands-on-testing</category>
      <category>ui-testing</category>
      <category>playwright</category>
      <category>selenium</category>
      <category>cypress</category>
      <description>What UI testing is and what it's for, how a browser test actually works, how to choose locators that don't break, why UI tests go flaky, and an honest comparison of Selenium, Playwright and Cypress.</description>
      <content:encoded><![CDATA[<p><a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Testing Basics</a> covered the ideas. This new series, <strong>Hands-On Testing</strong>, covers the practice: UI testing, API testing, security testing and load testing, each with a concepts post and a post where you build something that runs.</p>
<p>We start with the kind of testing most people picture when they hear the word: <strong>a program that opens a browser and uses your app like a person would</strong>.</p>
<h2>What is UI testing?</h2>
<p><strong>UI testing (user interface testing) checks an application through the same interface users see</strong>: pages, buttons, forms and screens, in a real browser or on a real device.</p>
<p>It sits at the top of the <a href="https://provalab.io/blog/types-of-software-testing/">testing pyramid</a>: the closest to a real user, and also the slowest and most expensive kind of test to write and maintain. That&#39;s why the pyramid says to have <em>a few</em> UI tests, for the journeys that matter most, not hundreds.</p>
<p>Two terms you&#39;ll hear alongside it:</p>
<ul>
<li><strong>End-to-end (E2E) testing</strong>: a UI test that follows a complete journey through the whole system, such as search → add to cart → pay → see confirmation. Most UI tests of any value are end-to-end tests.</li>
<li><strong>Visual testing</strong>: comparing screenshots against a known-good version to catch layout and styling changes. A specialised branch of UI testing.</li>
</ul>
<h2>What UI tests are good at, and bad at</h2>
<table>
<thead>
<tr>
<th>Good at</th>
<th>Bad at</th>
</tr>
</thead>
<tbody><tr>
<td>Proving a whole journey works, front to back</td>
<td>Pinpointing <em>why</em> something failed</td>
</tr>
<tr>
<td>Catching integration problems between front end and back end</td>
<td>Running fast (seconds per test, not milliseconds)</td>
</tr>
<tr>
<td>Checking what users actually see, in real browsers</td>
<td>Being stable: they depend on timing, data and the environment</td>
</tr>
<tr>
<td>Cross-browser and cross-device checks</td>
<td>Testing business logic (unit tests do that far more cheaply)</td>
</tr>
</tbody></table>
<p>The rule of thumb: <strong>test the logic with unit tests, the joins with <a href="https://provalab.io/blog/integration-testing/">integration tests</a>, and the journeys with UI tests.</strong> A UI test that checks fifty discount-code combinations is doing a unit test&#39;s job, fifty times slower.</p>
<h2>How a UI test works</h2>
<p><img src="https://provalab.io/blog/handson/ui-test-anatomy.svg" alt="A UI test script talks to a driver or automation protocol, which controls a real browser, which loads your app. The test repeats three steps: locate an element (the Sign in button, the Email box), act on it (type, click), and assert what the page shows (the heading reads Your orders). Most flaky tests fail at the locate step; pass or fail is decided at assert." width="880" height="440" loading="lazy"></p>
<p>Every UI test, in every tool, is the same three steps repeated:</p>
<ol>
<li><strong>Locate</strong> an element on the page: the email field, the &quot;Sign in&quot; button.</li>
<li><strong>Act</strong> on it: type, click, select, scroll.</li>
<li><strong>Assert</strong> what the page shows afterwards: the heading, the URL, a message, the absence of an error.</li>
</ol>
<p>Here is a complete sign-in test in Playwright for Python. Even if you&#39;ve never seen Playwright, you can read it:</p>
<pre><code class="language-python">def test_customer_can_sign_in(page):
    page.goto(&quot;https://staging.example.com/sign-in&quot;)
    page.get_by_label(&quot;Email&quot;).fill(&quot;test-user-2@example.com&quot;)
    page.get_by_label(&quot;Password&quot;).fill(&quot;correct-horse-battery&quot;)
    page.get_by_role(&quot;button&quot;, name=&quot;Sign in&quot;).click()
    expect(page.get_by_role(&quot;heading&quot;, name=&quot;Your orders&quot;)).to_be_visible()
</code></pre>
<p>Locate (<code>get_by_label</code>), act (<code>fill</code>, <code>click</code>), assert (<code>expect … to_be_visible</code>). We&#39;ll build and run this for real in Part 2.</p>
<p>Between the script and the browser sits a <strong>driver</strong> or <strong>automation protocol</strong>. Selenium uses the <strong>WebDriver</strong> standard, a W3C protocol every major browser supports. Playwright and Cypress talk to browsers more directly, which is a large part of why they feel faster.</p>
<h2>Locators: the skill that decides whether your tests survive</h2>
<p><strong>The single biggest cause of broken UI tests is the way they find elements.</strong> A test that looks for <code>div.col-md-6 &gt; form &gt; button.btn-primary</code> breaks the day a designer changes a class name, even though the app works perfectly.</p>
<p><img src="https://provalab.io/blog/handson/locator-ladder.svg" alt="A ladder of locator strategies, best first. 1, by role and name: get_by_role button named Sign in; what users see, survives redesigns. 2, by test id: get_by_test_id checkout-total; stable, added for testing. 3, by visible text or label; readable but breaks if wording changes. 4, by CSS selector tied to class names; breaks when styling changes. 5, by XPath tied to position; breaks on any layout change." width="880" height="440" loading="lazy"></p>
<p>The order of preference most teams settle on:</p>
<ol>
<li><strong>Role and accessible name</strong> (<code>get_by_role(&quot;button&quot;, name=&quot;Sign in&quot;)</code>). This is how a screen reader sees the page, so it matches what users see and ignores styling. It also quietly pushes your app towards being accessible.</li>
<li><strong>Test IDs</strong> (<code>data-testid=&quot;checkout-total&quot;</code>), attributes added to the HTML purely for tests. Very stable; the only cost is asking developers to add them.</li>
<li><strong>Visible text and labels</strong>. Readable, but tied to wording and language.</li>
<li><strong>CSS selectors</strong>. Fine for structure that won&#39;t change; fragile when tied to styling classes.</li>
<li><strong>XPath by position</strong>. Avoid. It breaks whenever anything moves.</li>
</ol>
<p>A good test reads like a description of what a user does. If you can&#39;t tell what a locator points at without opening the page, it&#39;s the wrong locator.</p>
<h2>Why UI tests go flaky, and what to do about it</h2>
<p>A <strong>flaky test</strong> passes and fails on different runs with no code change. UI tests are the flakiest kind, for predictable reasons:</p>
<table>
<thead>
<tr>
<th>Cause</th>
<th>What it looks like</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Timing</strong></td>
<td>The test clicks before the button exists, or asserts before data loads</td>
<td>Use the tool&#39;s built-in waiting (Playwright and Cypress wait for elements automatically). Never use fixed sleeps</td>
</tr>
<tr>
<td><strong>Test data</strong></td>
<td>Two tests use the same account and collide; yesterday&#39;s order changes today&#39;s counts</td>
<td>Each test creates or picks its own data; reset state between tests</td>
</tr>
<tr>
<td><strong>Environment</strong></td>
<td>The staging server is slow or restarting</td>
<td>Run a <a href="https://provalab.io/blog/smoke-vs-sanity-testing/">smoke test</a> first; retry only network-level failures</td>
</tr>
<tr>
<td><strong>Order dependence</strong></td>
<td>Test B only passes if test A ran first</td>
<td>Every test signs in and sets up what it needs itself</td>
</tr>
<tr>
<td><strong>Animations and pop-ups</strong></td>
<td>A cookie banner covers the button; a fade-in isn&#39;t finished</td>
<td>Dismiss banners in setup; assert on the final state, not the animation</td>
</tr>
<tr>
<td><strong>Fragile locators</strong></td>
<td>The class name changed</td>
<td>Climb the locator ladder above</td>
</tr>
</tbody></table>
<p>Flaky tests are dangerous, not just annoying: once people learn to re-run red tests until they pass, real failures get re-run away too (see <a href="https://provalab.io/blog/test-runs-and-test-results/">Part 17</a>). Treat a flaky test as a bug in the test suite and fix it the same week.</p>
<h2>Selenium vs Playwright vs Cypress</h2>
<p>The three tools almost every team chooses between:</p>
<table>
<thead>
<tr>
<th></th>
<th><strong>Selenium</strong></th>
<th><strong>Playwright</strong></th>
<th><strong>Cypress</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>First released</strong></td>
<td>2004</td>
<td>2020</td>
<td>2017</td>
</tr>
<tr>
<td><strong>Languages</strong></td>
<td>Java, Python, C#, JavaScript, Ruby, Kotlin</td>
<td>JavaScript/TypeScript, Python, Java, C#</td>
<td>JavaScript/TypeScript only</td>
</tr>
<tr>
<td><strong>Browsers</strong></td>
<td>All major browsers, via WebDriver</td>
<td>Chromium, Firefox, WebKit (Safari engine)</td>
<td>Chrome-family, Firefox, WebKit (experimental)</td>
</tr>
<tr>
<td><strong>How it drives the browser</strong></td>
<td>WebDriver protocol (a W3C standard)</td>
<td>Direct protocol connections; one API for all three engines</td>
<td>Runs <em>inside</em> the browser alongside your app</td>
</tr>
<tr>
<td><strong>Automatic waiting</strong></td>
<td>No; you write explicit waits</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td><strong>Multiple tabs and origins</strong></td>
<td>Yes</td>
<td>Yes</td>
<td>Limited</td>
</tr>
<tr>
<td><strong>Mobile</strong></td>
<td>Via Appium (real devices)</td>
<td>Device emulation in desktop browsers</td>
<td>Viewport emulation only</td>
</tr>
<tr>
<td><strong>Built-in trace, video, screenshots</strong></td>
<td>Screenshots; the rest via add-ons</td>
<td>Yes, all three, with a trace viewer</td>
<td>Yes</td>
</tr>
<tr>
<td><strong>Best for</strong></td>
<td>Large existing suites, language flexibility, the widest browser and device coverage</td>
<td>New projects wanting speed and reliability across all engines</td>
<td>Front-end teams working in JavaScript who want a fast developer experience</td>
</tr>
</tbody></table>
<p>Some honest observations:</p>
<ul>
<li><strong>Selenium</strong> is the industry standard with the largest ecosystem and the most job listings. Its WebDriver protocol is why it works everywhere, including real mobile devices through Appium. Its main cost is that you manage waiting yourself, which is where many flaky Selenium suites come from.</li>
<li><strong>Playwright</strong> was built by the team that originally created Puppeteer at Google, after they moved to Microsoft. It&#39;s fast, waits automatically, tests all three browser engines with one API, and its trace viewer (a recording of every step, with screenshots and network calls) is the best debugging tool of the three. It is the default choice for new projects today.</li>
<li><strong>Cypress</strong> has the most pleasant developer experience for front-end engineers: tests run in the browser with a live, time-travelling view. The trade-offs are JavaScript-only, weaker multi-tab and cross-origin support, and a paid service for parallel runs and recordings.</li>
</ul>
<p><strong>If you&#39;re starting from nothing, pick Playwright.</strong> If your team already has a Selenium suite that works, there is no need to rewrite it. If your team is all front-end JavaScript developers, Cypress will make them happiest.</p>
<h2>Where UI tests run</h2>
<ul>
<li><strong>On your laptop</strong> while writing them.</li>
<li><strong>In the CI pipeline</strong> on every change, usually headless (no visible browser window), as described in <a href="https://provalab.io/blog/testing-in-agile-and-ci-cd/">Part 23 of Testing Basics</a>.</li>
<li><strong>On a browser grid or device cloud</strong> when you need many browsers, operating systems and real phones at once, far more than one machine can hold. This is what services like ProvaLab&#39;s QA Infrastructure, BrowserStack and Sauce Labs provide.</li>
</ul>
<h2>Seven habits of good UI tests</h2>
<ol>
<li><strong>Test journeys, not pages.</strong> &quot;A new customer can sign up and place an order&quot; beats &quot;the sign-up page has an email field&quot;.</li>
<li><strong>Keep them few and valuable.</strong> Push everything else down the pyramid.</li>
<li><strong>Use role-based locators or test IDs.</strong></li>
<li><strong>Make every test independent:</strong> its own data, its own sign-in.</li>
<li><strong>Never sleep.</strong> Wait for a condition, not a number of seconds.</li>
<li><strong>Assert on what the user sees</strong>, not on internal state.</li>
<li><strong>Record on failure.</strong> A screenshot, video or trace turns a red test into a bug report in seconds.</li>
</ol>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/first-playwright-test/">Part 2</a></strong> we build real tests like the one above: install Playwright, write the test, run it headless and headed, watch it fail, read the trace, and add it to a pipeline.</p>
<p>ProvaLab&#39;s QA Infrastructure runs Playwright and Selenium tests on real browsers and devices in the cloud, records every session, and hands the recording and logs back to your coding agent when a test fails. See it in the <a href="https://provalab.io/#stage-infra">two-minute product tour</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What is software testing, and why does every team need it?</title>
      <link>https://provalab.io/blog/what-is-software-testing/</link>
      <guid isPermaLink="true">https://provalab.io/blog/what-is-software-testing/</guid>
      <pubDate>Thu, 29 Oct 2026 00:00:00 GMT</pubDate>
      <dc:creator>ProvaLab team</dc:creator>
      <category>testing-basics</category>
      <category>software-testing</category>
      <description>A plain-language introduction to software testing: what it is, what it checks, when it happens, who does it, and the myths that trip teams up.</description>
      <content:encoded><![CDATA[<p>Every app you use today, your bank, your food delivery, the page you are reading, was tested before it reached you. Usually you only notice testing when it was skipped: a payment that fails, a page that never loads, a &quot;something went wrong&quot; at the worst moment.</p>
<p>This is Part 1 of our <strong>Testing Basics</strong> series. No prior knowledge needed. By the end you will know what software testing is, what it checks, when it happens and who does it.</p>
<h2>What is software testing?</h2>
<p><strong>Software testing is checking that software does what it should, and doesn&#39;t do what it shouldn&#39;t.</strong></p>
<p>That sentence has two halves, and both matter:</p>
<ul>
<li><strong>Does what it should:</strong> a user can sign up, add items to a cart and pay.</li>
<li><strong>Doesn&#39;t do what it shouldn&#39;t:</strong> a user can&#39;t see someone else&#39;s orders, the app doesn&#39;t crash on an empty search, and a wrong password doesn&#39;t let anyone in.</li>
</ul>
<p>A <strong>test</strong> is one small check of that kind. It has three parts:</p>
<ol>
<li><strong>A starting point:</strong> &quot;A signed-in user with one item in their cart.&quot;</li>
<li><strong>An action:</strong> &quot;They click Pay.&quot;</li>
<li><strong>An expected result:</strong> &quot;They see the order confirmation, and the cart is empty.&quot;</li>
</ol>
<p>If what actually happens matches the expected result, the test <strong>passes</strong>. If not, it <strong>fails</strong>, and you have found a <strong>bug</strong>: a difference between what the software does and what it should do.</p>
<h2>What does testing check?</h2>
<p>Testing answers four big questions about your software:</p>
<p><img src="https://provalab.io/blog/basics/what-testing-checks.svg" alt="Testing answers four questions. Does it work: functional testing. Is it fast enough: performance and load testing. Is it safe: security testing. Is it easy to use: usability and accessibility testing." width="880" height="470" loading="lazy"></p>
<p>The first question, <strong>&quot;does it work?&quot;</strong>, is called <strong>functional testing</strong>. The other three are <strong>non-functional testing</strong>: they check <em>how well</em> the software works, not just whether it does. Each one gets its own posts later in this series.</p>
<h2>Why does every team need testing?</h2>
<ul>
<li><strong>Users leave quickly.</strong> If the checkout breaks, most people don&#39;t report it. They just go to a competitor.</li>
<li><strong>Bugs get more expensive the later you find them.</strong> A mistake caught while writing the code takes minutes to fix. The same mistake found by a customer means support tickets, an urgent fix, a new release and lost trust.</li>
<li><strong>Security problems are business problems.</strong> One leaked customer record can cost far more than years of testing.</li>
<li><strong>Changes break old things.</strong> Every new feature can break an old one. Tests that run on every change catch that, so the team can move fast without fear.</li>
<li><strong>Code is written faster than ever.</strong> With AI coding assistants, teams produce far more code. Checking it has to keep up.</li>
</ul>
<h2>When does testing happen?</h2>
<p>A common mistake is to test only at the end, just before release. Good teams test at every stage:</p>
<p><img src="https://provalab.io/blog/basics/test-early.svg" alt="Testing at every stage: test cases from requirements while planning, unit tests while writing code, automated checks during review, UI, API, security and load tests before release, and monitoring in production. The later a bug is found, the more work it takes to fix." width="880" height="400" loading="lazy"></p>
<p>This idea is often called <strong>&quot;shift left&quot;</strong>: move testing earlier (to the left) on the timeline, where bugs are cheapest to fix.</p>
<h2>Who does the testing?</h2>
<ul>
<li><strong>Developers</strong> write <strong>unit tests</strong> that check small pieces of their own code.</li>
<li><strong>QA engineers</strong> (quality assurance) plan what to test, test like a real user would, and look for the cases nobody thought of.</li>
<li><strong>Automation engineers</strong> turn repeated checks into code that runs by itself, on every change.</li>
<li><strong>Security and performance specialists</strong> test the harder questions: can it be attacked, and does it hold up under heavy traffic?</li>
<li><strong>AI agents</strong> are the newest member of the team: they can now write and run tests, and read the results.</li>
</ul>
<p>In a small team, one person may do all of these. What matters is that each job gets done, not who has which title.</p>
<h2>Manual and automated testing</h2>
<p>Testing can be done in two ways:</p>
<ul>
<li><strong>Manual testing:</strong> a person uses the app and checks the result by eye. Best for new features, design and &quot;does this feel right?&quot;</li>
<li><strong>Automated testing:</strong> code performs the checks. Best for anything you need to check again and again, like &quot;can users still sign in?&quot; on every release.</li>
</ul>
<p>Most teams use both. We cover automation in detail in <a href="https://provalab.io/blog/manual-vs-automated-testing/">Part 3</a>.</p>
<h2>Four myths about testing</h2>
<ol>
<li><strong>&quot;Testing proves there are no bugs.&quot;</strong> It can&#39;t. Testing shows bugs are there; it never proves they are all gone. Its job is to find the important ones before users do.</li>
<li><strong>&quot;Testing is the QA team&#39;s job.&quot;</strong> Quality is everyone&#39;s job. Developers, reviewers and testers each catch different problems.</li>
<li><strong>&quot;We&#39;ll test at the end.&quot;</strong> By the end, bugs are tangled together and expensive. Test as you go.</li>
<li><strong>&quot;100% test coverage means we&#39;re safe.&quot;</strong> Coverage tells you which lines of code ran during tests, not whether the tests checked the right things.</li>
</ol>
<h2>Key terms</h2>
<table>
<thead>
<tr>
<th>Term</th>
<th>Meaning</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Test case</strong></td>
<td>One check: a starting point, an action and an expected result</td>
</tr>
<tr>
<td><strong>Bug (defect)</strong></td>
<td>A difference between what the software does and what it should do</td>
</tr>
<tr>
<td><strong>Test suite</strong></td>
<td>A group of related test cases, such as &quot;all checkout tests&quot;</td>
</tr>
<tr>
<td><strong>Test run</strong></td>
<td>One time a set of tests is run, with a pass or fail result for each</td>
</tr>
<tr>
<td><strong>Regression</strong></td>
<td>Something that used to work and is now broken by a change</td>
</tr>
</tbody></table>
<h2>What&#39;s next</h2>
<p>In <strong><a href="https://provalab.io/blog/types-of-software-testing/">Part 2</a></strong> we look at the types of testing (unit, integration, system and acceptance tests) and when to use each.</p>
<p>Want to see testing done by an AI agent on a real app? Watch the <a href="https://provalab.io/#stage-tms">two-minute product tour</a>. ProvaLab keeps your test cases, runs them on real browsers and devices, and records every result in one place.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
