<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ifeanyi Ejindu on app testing]]></title><description><![CDATA[Ifeanyi Ejindu on app testing]]></description><link>https://ifeanyiejindu.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Ifeanyi Ejindu on app testing</title><link>https://ifeanyiejindu.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 07 Oct 2026 12:46:07 GMT</lastBuildDate><atom:link href="https://ifeanyiejindu.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your testing isn't the problem. Your hand-offs are.]]></title><description><![CDATA[Ask a team why a release slipped and you rarely hear "we didn't test." You hear something closer to "we didn't know." Nobody knew the bug had been fixed. Nobody knew which version of the spreadsheet w]]></description><link>https://ifeanyiejindu.hashnode.dev/your-testing-isn-t-the-problem-your-hand-offs-are</link><guid isPermaLink="true">https://ifeanyiejindu.hashnode.dev/your-testing-isn-t-the-problem-your-hand-offs-are</guid><category><![CDATA[Testing]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[QA]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Ifeanyi Ejindu]]></dc:creator><pubDate>Sat, 03 Oct 2026 10:42:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6abf93e14874bd8ceb3b361e/b6c136eb-b346-44d0-8f06-43b0adb170ed.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ask a team why a release slipped and you rarely hear "we didn't test." You hear something closer to "we didn't know." Nobody knew the bug had been fixed. Nobody knew which version of the spreadsheet was current. Nobody knew whether the thing the customer reported last week was the same thing a tester found yesterday.</p>
<p>The testing happened. The information just didn't travel.</p>
<h2>Where it breaks down</h2>
<p>Most teams will recognise at least one of these:</p>
<ul>
<li><p><strong>Bugs live in chat.</strong> A screenshot in a thread, a message in a DM. By Friday it has scrolled out of sight, and nobody can find the steps to reproduce it.</p>
</li>
<li><p><strong>Spreadsheets drift.</strong> Someone copies the test sheet for the new release. Someone else keeps editing the old one. Nobody is sure which row is the current one.</p>
</li>
<li><p><strong>Fixes go unannounced.</strong> A developer merges the fix and moves on. The tester who found it never hears, so it's never checked again, or it's checked weeks later.</p>
</li>
<li><p><strong>Release day is a guess.</strong> A manager asks whether it's ready. The honest answer is "probably," because nobody can see the whole picture in one place.</p>
</li>
</ul>
<p>None of these are failures of skill. They're failures of hand-off: work passing between people and tools, and losing something on the way.</p>
<h2>Turn the hand-offs into a loop</h2>
<p>The fix isn't another place to copy things into. It's connecting the places your team already works, so the status moves by itself.</p>
<p>That's how QA Runbook works with GitHub, which is where most teams' code and developer work already lives:</p>
<ol>
<li><p><strong>A tester raises the bug</strong> in QA Runbook, in their own words, against the check it's about or on its own for a live app. It gets a short code, like QA-13.</p>
</li>
<li><p><strong>It's filed in GitHub automatically</strong>, as an issue with the code in the title and a link back to every detail. Developers see it where they already work.</p>
</li>
<li><p><strong>The developer fixes it</strong>, and the pull request says so: "Fixes #8" or "Fixes QA-13". When it merges, GitHub closes the issue.</p>
</li>
<li><p><strong>QA Runbook marks it fixed</strong> on its own, links the pull request, and tells the tester it's ready to check.</p>
</li>
<li><p><strong>The tester confirms it</strong> on the real app. Fixed doesn't count until someone has seen it work.</p>
</li>
</ol>
<p>Nobody had to chase anybody, and everyone saw the same status in both places.</p>
<h2>The same loop for every kind of testing</h2>
<p>The loop doesn't care what you're testing:</p>
<ul>
<li><p><strong>A release you're about to ship:</strong> every check, every result per platform, and who found what.</p>
</li>
<li><p><strong>A new feature:</strong> a plan for just that feature, worked through by the people who know it.</p>
</li>
<li><p><strong>A live app:</strong> bugs from users and support, tracked from report to confirmed fix.</p>
</li>
</ul>
<h2>It fits the rest of the team</h2>
<p>Testing touches more people than testers:</p>
<ul>
<li><p><strong>Your AI assistant</strong> can connect over MCP, read what's failing, and record the results of the checks it can run itself.</p>
</li>
<li><p><strong>Your support desk</strong> can send customer reports straight in through the API, with the reporter attached.</p>
</li>
<li><p><strong>Managers</strong> get one report of what passed, what failed and what's fixed, ready to download or share.</p>
</li>
</ul>
<h2>You don't need to integrate everything</h2>
<p>It's tempting to think a tool is only useful once it connects to everything. In practice, most teams' work meets in one place: GitHub. Get that connection right, and the loop from bug to verified fix closes itself.</p>
<p>That's the whole idea: testing, simplified and effective.</p>
<hr />
<p><strong>See it in two and a half minutes, all real footage:</strong> <a href="https://youtu.be/fFviMrot078">https://youtu.be/fFviMrot078</a></p>
<p>Set it up with the guide: <a href="https://qarunbook.com/docs/guides/github">qarunbook.com/docs/guides/github</a>. There's a free plan, no card needed.</p>
]]></content:encoded></item><item><title><![CDATA[How to write test cases someone else can run]]></title><description><![CDATA[Most test cases are written by the person who built the feature, in a hurry, for themselves. They read fine to the author and fall apart in anyone else's hands: a step that assumes you know where the ]]></description><link>https://ifeanyiejindu.hashnode.dev/how-to-write-test-cases-someone-else-can-run</link><guid isPermaLink="true">https://ifeanyiejindu.hashnode.dev/how-to-write-test-cases-someone-else-can-run</guid><category><![CDATA[Testing]]></category><category><![CDATA[QA]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[mobile app development]]></category><dc:creator><![CDATA[Ifeanyi Ejindu]]></dc:creator><pubDate>Fri, 02 Oct 2026 13:22:06 GMT</pubDate><content:encoded><![CDATA[<p>Most test cases are written by the person who built the feature, in a hurry, for themselves. They read fine to the author and fall apart in anyone else's hands: a step that assumes you know where the button is, an expected result that says "works correctly", a check that only passes if you ran the one before it.</p>
<p>This post is about writing them so somebody else can run them and reach the same answer you would. That matters more now than it used to, because "somebody else" is increasingly a teammate on a different phone, a client doing acceptance testing, or an AI assistant working through your plan.</p>
<h2>What a test case is (and what it isn't)</h2>
<p>A test case checks <strong>one behaviour</strong> of the app. It says what has to be true before you start, the steps to take, and the result you should see at the end. If the result matches, it passes. If it doesn't, you have found a bug, and the test case is already most of the bug report.</p>
<p>Two terms get mixed up with it:</p>
<ul>
<li><p>A <strong>test scenario</strong> is broader: "a customer resets their password". One scenario usually needs several test cases: the reset that works, the link used twice, the email with no account behind it.</p>
</li>
<li><p>A <strong>checklist item</strong> is shorter: "password reset works". Fine as a reminder for the person who wrote it. Not something a new tester can run, because it doesn't say what "works" looks like.</p>
</li>
</ul>
<p>A <strong>test plan</strong> is the whole collection: every test case for the app, grouped into sections, with a column for each platform you ship on.</p>
<h2>The six fields you actually need</h2>
<table>
<thead>
<tr>
<th>Field</th>
<th>Why it's there</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ID</strong></td>
<td>A short, stable handle like <code>PAY-02</code>. People quote it in messages and bug reports, so it never changes once written.</td>
</tr>
<tr>
<td><strong>Journey</strong></td>
<td>What is being checked, in the customer's words: "A declined card is handled", not "Validate payment error state".</td>
</tr>
<tr>
<td><strong>Preconditions</strong></td>
<td>The state you must be in first, including test data: signed in or out, a saved card, aeroplane mode on.</td>
</tr>
<tr>
<td><strong>Steps</strong></td>
<td>Numbered, one action each, with buttons named as they appear on screen.</td>
</tr>
<tr>
<td><strong>Expected result</strong></td>
<td>What you should see, concretely enough to decide pass or fail without asking anyone.</td>
</tr>
<tr>
<td><strong>Platforms</strong></td>
<td>One column per platform (web, Android, iOS). Write <code>N/A</code> where the feature doesn't exist.</td>
</tr>
</tbody></table>
<p>Templates often add actual result, status, tester and date. Those belong to a <em>run</em>, not to the case, and they change on every build. Priority is better handled by section order (below).</p>
<h2>Writing one, step by step</h2>
<ol>
<li><p><strong>Start from what the user is trying to do.</strong> Read the feature or the acceptance criteria and write down the outcome a person wants: "pay for what is in my basket". That's your scenario.</p>
</li>
<li><p><strong>List the cases before writing any of them.</strong> One line for the path that should work, then one for each way it can be refused or go wrong: wrong input, missing permission, no network, an expired session, the button pressed twice. The second list is where bugs hide.</p>
</li>
<li><p><strong>Look at the edges of every input.</strong> If a password needs at least 8 characters, test 7 and 8, not 3 and 20. And when many inputs should behave the same way, one example from each group is enough.</p>
</li>
<li><p><strong>Write the preconditions.</strong> Everything the tester needs before step one. If a case needs a declining card or a used reset link, say so here.</p>
</li>
<li><p><strong>Write the steps.</strong> One action per step, in order, using on-screen names.</p>
</li>
<li><p><strong>Write the expected result.</strong> What's on screen, what's kept, what's cleared, and what did <em>not</em> happen.</p>
</li>
<li><p><strong>Mark the platforms.</strong> Blank if it needs testing there, <code>N/A</code> if the feature doesn't exist there.</p>
</li>
<li><p><strong>Have someone else run it once.</strong> If they ask you a question, the answer belongs in the test case.</p>
</li>
</ol>
<h2>Expected results are where test cases go wrong</h2>
<p>"The error is handled" passes as soon as <em>any</em> error appears. Compare:</p>
<blockquote>
<p><strong>Weak:</strong> An error is shown for a declined card.</p>
<p><strong>Strong:</strong> A message says the card was declined and offers another way to pay. The basket keeps its item. No order appears under Orders.</p>
</blockquote>
<p>The strong version catches three bugs the weak one lets through: a vague message, a basket that empties itself, and an order created for a payment that failed. Put the judgement in the test case, not in the tester's head.</p>
<h2>Five worked examples</h2>
<p>These are from a shop app tested on web, Android and iOS. Each covers a journey nearly every app has.</p>
<h3>SIGN-01: Sign up with an email address</h3>
<ul>
<li><p><strong>Preconditions:</strong> Signed out. No account exists for the email you will use.</p>
</li>
<li><p><strong>Steps:</strong> 1. Open the app. 2. Tap Create account. 3. Enter a name, a new email address and a password of at least 8 characters. 4. Tap Create account.</p>
</li>
<li><p><strong>Expected:</strong> The home screen opens with the name you entered at the top. A verification email arrives within a minute.</p>
</li>
</ul>
<p>The preconditions matter: an email that already has an account makes this a different case. The expected result checks two things a quick look misses, that the name was saved and that the email actually arrives.</p>
<h3>RESET-01: Reset a forgotten password</h3>
<ul>
<li><p><strong>Preconditions:</strong> Signed out. An existing account whose inbox you can open.</p>
</li>
<li><p><strong>Steps:</strong> 1. Tap Sign in, then Forgot password. 2. Enter the account's email and submit. 3. Open the link in the reset email. 4. Set a new password. 5. Sign in with the new password.</p>
</li>
<li><p><strong>Expected:</strong> The new password signs you in. Signing in with the old password fails with the usual wrong-password message.</p>
</li>
</ul>
<p>The second half of the expected result is what makes this a real test. A reset that sets the new password but leaves the old one working looks fine from the success screen.</p>
<h3>PAY-02: A declined card is handled</h3>
<ul>
<li><p><strong>Preconditions:</strong> Signed in. A test card that will decline. One item in the basket.</p>
</li>
<li><p><strong>Steps:</strong> 1. Open the basket. 2. Tap Checkout. 3. Enter the declining card. 4. Tap Pay.</p>
</li>
<li><p><strong>Expected:</strong> A message says the card was declined and offers another way to pay. The basket keeps its item. No order appears under Orders.</p>
</li>
</ul>
<p>Most payment providers publish test card numbers that decline on purpose. Name the one you use in the preconditions.</p>
<h3>NET-02: The connection drops during payment</h3>
<ul>
<li><p><strong>Preconditions:</strong> Signed in. A saved card. One item in the basket.</p>
</li>
<li><p><strong>Steps:</strong> 1. Tap Pay. 2. Turn on aeroplane mode before the confirmation appears. 3. Turn it off again. 4. Open Orders.</p>
</li>
<li><p><strong>Expected:</strong> The app says the payment may not have gone through and how to check. Orders shows at most one order. Paying again does not charge twice.</p>
</li>
</ul>
<p>The offline case that finds real bugs isn't opening the app in aeroplane mode. It's losing the signal halfway through something that matters. The expected result accepts that the app may not know what happened, and asks only that it says so and never charges twice.</p>
<h3>PERM-02: Camera denied is survivable (Android, iOS; N/A on web)</h3>
<ul>
<li><p><strong>Preconditions:</strong> Signed in. Camera access denied for the app in system settings.</p>
</li>
<li><p><strong>Steps:</strong> 1. Open Profile. 2. Tap Change photo. 3. Choose Take photo.</p>
</li>
<li><p><strong>Expected:</strong> A message says camera access is off, with a button that opens the app's settings and an option to pick from the photo library instead. No crash, and no repeated prompt.</p>
</li>
</ul>
<p>Permissions have three states: granted, denied, and granted later in settings. Teams test the first. This tests the second.</p>
<h2>What a good test case looks like</h2>
<ul>
<li><p><strong>It checks one behaviour.</strong> If the title needs an "and", it's probably two cases.</p>
</li>
<li><p><strong>It stands on its own.</strong> Anything it depends on goes in the preconditions, not in "run case 6 first".</p>
</li>
<li><p><strong>It gives the same answer every time.</strong> Two people on the same build should reach the same result.</p>
</li>
<li><p><strong>One platform doesn't stand in for all of them.</strong> A pass on an iPhone says nothing about Android.</p>
</li>
</ul>
<h2>Organising them</h2>
<p>Group cases by journey (sign-up, password reset, checkout, offline, permissions), not by who wrote them or which sprint they arrived in. Give each section a short reference like <code>PAY</code> and number the cases inside it. New cases get the next number; old numbers are never reused.</p>
<p>Put the sections that matter most first. If sign-in and payment break, nothing else matters, and that ordering does the job of a priority column at a glance. Keep platform differences in columns, not in three nearly identical copies of the same case.</p>
<h2>The template</h2>
<p>Here's the shape as Markdown, which pastes straight into a repo, a doc or a spreadsheet:</p>
<pre><code class="language-markdown">## Checkout (PAY)

| ID | Journey | Preconditions | Steps | Expected result | WEB | AND | IOS |
|---|---|---|---|---|---|---|---|
| PAY-01 | Pay with a saved card | Signed in. A saved card that will succeed. One item in the basket | 1. Open the basket. 2. Tap Checkout. 3. Choose the saved card. 4. Tap Pay. | A confirmation shows an order number and the same total as the basket. A receipt email arrives. The basket is empty. | | | |
| PAY-03 | Tapping Pay twice charges once | Signed in. A saved card. One item in the basket | 1. Go to checkout. 2. Tap Pay twice, quickly. | One order is created and the card is charged once. The button shows it is working after the first tap. | | | |
</code></pre>
<p>The full version, with all 13 example cases across five sections, is in the original guide: <a href="https://qarunbook.com/guides/how-to-write-test-cases?utm_source=hashnode&amp;utm_medium=community&amp;utm_campaign=article1">How to write test cases, with examples and a template</a>. Full disclosure: I build qarunbook, a test management tool for web and mobile apps, and that format is what it imports. It doesn't run automated tests; it keeps the cases, the result on each platform, and the bugs they turn up. The template works just as well in a spreadsheet.</p>
<p>If you write test cases differently, I'd like to hear how, especially how you handle expected results for things that can partly succeed, like the dropped-connection payment above.</p>
]]></content:encoded></item></channel></rss>