05 · Manual Testing in Practice¶
You can't test everything. This module covers the formal techniques that let you choose a small set of test cases with a high chance of finding defects — equivalence partitioning, boundary value analysis, decision tables, state transition testing — plus exploratory testing, the skill that finds what your formal cases never anticipated.
These are black-box techniques: they work from the specification, without seeing the code.
1. Equivalence Partitioning (EP)¶
Principle: divide the input domain into groups where every value in a group should be treated identically by the system. Test one value from each group.
If 25 and 26 go down exactly the same code path, testing both adds cost and
no information.
Worked example — age field on an insurance application¶
Requirement: applicants must be 18–65 inclusive. Under 18 → "Not eligible — minimum age is 18." Over 65 → "Not eligible — maximum age is 65." Non-numeric input → "Enter a valid age."
| Partition | Range / description | Valid? | One representative value |
|---|---|---|---|
| P1 | Below minimum | Invalid | 10 |
| P2 | Eligible range | Valid | 35 |
| P3 | Above maximum | Invalid | 72 |
| P4 | Zero / negative | Invalid | -5 |
| P5 | Non-numeric text | Invalid | abc |
| P6 | Empty | Invalid | `` (blank) |
| P7 | Decimal | Invalid (probably — ask) | 25.5 |
| P8 | Whitespace-padded number | Valid? (ask) | 35 |
Eight tests instead of the 100+ you'd get by trying every age. And note P7 and P8: partitioning surfaces specification gaps as a side effect.
Partitioning non-numeric inputs¶
EP is not only for numbers. Partition by how the system treats the value:
- Country dropdown → domestic / EU / rest of world / sanctioned country / none selected.
- File upload → allowed type, disallowed type, no extension, zero-byte file, file over the size limit, correct extension but corrupt contents.
- Payment method → credit card, debit card, wallet, expired card, card requiring 3-D Secure, card issued in a different currency.
- User role → anonymous, standard, premium, admin, suspended.
Partitions must be genuinely equivalent
If 35 and 55 hit different premium-calculation brackets, they are not
in the same partition, however similar the input looks. Partition by
behaviour, never by appearance.
2. Boundary Value Analysis (BVA)¶
Principle: defects cluster at the edges of partitions, because that's where
< gets written instead of <=. Test the boundary and its immediate neighbours.
For each boundary, test three values: min−1, min, min+1 and max−1, max, max+1. (The two-value variant tests just the pair straddling each boundary.)
Worked example — the same 18–65 age field¶
| Value | Partition | Expected result | Why it's here |
|---|---|---|---|
17 |
Below min | Rejected — "minimum age is 18" | min − 1 |
18 |
Valid | Accepted | min — the classic off-by-one |
19 |
Valid | Accepted | min + 1 |
64 |
Valid | Accepted | max − 1 |
65 |
Valid | Accepted | max — the classic off-by-one |
66 |
Above max | Rejected — "maximum age is 65" | max + 1 |
Six cases. Bugs found by this exact pattern in the wild: an application rejecting
18-year-olds because the code says age > 18 instead of age >= 18. It is the
single highest-yield technique in black-box testing.
Worked example — a text field with a length limit¶
Requirement: the "Display name" field accepts 3 to 30 characters.
| Length | Input | Expected |
|---|---|---|
| 2 | ab |
Rejected — "at least 3 characters" |
| 3 | abc |
Accepted |
| 4 | abcd |
Accepted |
| 29 | 29 chars | Accepted |
| 30 | 30 chars | Accepted |
| 31 | 31 chars | Rejected — "at most 30 characters" |
And the boundary-adjacent questions worth asking: is the limit enforced on the client, the server, or both? Does the field truncate at 30 rather than reject? Do emoji count as one character or four? Does a trailing space count?
Boundaries hide everywhere¶
| Domain | Boundaries worth testing |
|---|---|
| Numbers | 0, 1, −1, max int, floating-point rounding at 0.005 |
| Strings | empty, 1 char, exactly the limit, limit+1, only whitespace |
| Dates | 1st and last day of month, 29 Feb in a leap year, 28→1 Mar in a non-leap year, DST change day, year end, timezone midnight |
| Collections | empty list, 1 item, page-size boundary (e.g. 10, 11 items with 10-per-page), max items |
| Money | 0.00, 0.01, the free-shipping threshold exactly, currency rounding on 1/3 splits |
| Time | session timeout at exactly the limit, token expiry, request timeout |
3. Combining EP and BVA¶
The two techniques are complementary and used together: EP tells you which groups exist; BVA tells you where within each group to test.
Worked example — a shipping-cost rule.
Orders under ₹500: ₹60 shipping. ₹500–₹1,999: ₹30 shipping. ₹2,000 and above: free shipping. Orders cannot exceed ₹1,00,000.
Partitions (EP): < 500 · 500–1999 · 2000–100000 · > 100000 · invalid
(negative, zero, non-numeric).
Boundaries (BVA) at 500, 2000 and 100000:
| Order value | Expected shipping | Technique |
|---|---|---|
250 |
₹60 | EP — mid-partition |
499 |
₹60 | BVA — min−1 |
500 |
₹30 | BVA — the boundary |
501 |
₹30 | BVA — min+1 |
1200 |
₹30 | EP — mid-partition |
1999 |
₹30 | BVA — max−1 |
2000 |
Free | BVA — the boundary |
2001 |
Free | BVA — max+1 |
99999 |
Free | BVA — max−1 |
100000 |
Free | BVA — the boundary |
100001 |
Rejected | BVA — max+1 |
0 |
Rejected / empty cart | EP — invalid |
-100 |
Rejected | EP — invalid |
Thirteen cases give near-complete confidence in a rule that has, in principle, ten million valid inputs. That is what test design buys you.
4. Decision tables¶
When output depends on a combination of conditions, EP and BVA aren't enough — you need to cover the combinations systematically. A decision table lays out every combination of conditions and the action for each.
Worked example — loan approval¶
Approve if: credit score ≥ 700 and employed and existing debt is below 40% of income. If the score is 650–699 but the applicant is employed with debt below 40%, refer to a manual review. Anything else is rejected. Anyone unemployed is rejected regardless of score.
| Rule | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| Conditions | ||||||||
| Credit score ≥ 700 | Y | Y | Y | Y | N | N | N | N |
| Credit score 650–699 | N | N | N | N | Y | Y | Y | N |
| Employed | Y | Y | N | N | Y | Y | N | – |
| Debt < 40% of income | Y | N | Y | N | Y | N | – | – |
| Actions | ||||||||
| Approve | ✔ | |||||||
| Refer to manual review | ✔ | |||||||
| Reject | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
Eight rules → eight test cases, and each one has concrete data attached:
| TC | Score | Employed | Debt ratio | Expected |
|---|---|---|---|---|
| TC-LOAN-01 | 720 | Yes | 25% | Approved |
| TC-LOAN-02 | 720 | Yes | 55% | Rejected |
| TC-LOAN-03 | 720 | No | 25% | Rejected |
| TC-LOAN-04 | 720 | No | 55% | Rejected |
| TC-LOAN-05 | 670 | Yes | 25% | Manual review |
| TC-LOAN-06 | 670 | Yes | 55% | Rejected |
| TC-LOAN-07 | 670 | No | 25% | Rejected |
| TC-LOAN-08 | 610 | Yes | 25% | Rejected |
Combine with BVA on the score boundaries — 649, 650, 699, 700 — and you have a rigorously covered rule.
Collapsing the table
Three binary conditions give 2³ = 8 combinations; four give 16. When a
condition is irrelevant to the outcome (marked – above — unemployed
applicants are rejected whatever their debt ratio), collapse those columns
into one rule. This keeps the table readable as conditions multiply.
5. State transition testing¶
Use it when the system's response depends on what happened before — orders, accounts, subscriptions, sessions, anything with a status field.
Worked example — account lockout¶
An account locks after 3 consecutive failed login attempts. A successful login resets the counter. A locked account unlocks after 30 minutes or via a password reset.
States: Active · Active (1 fail) · Active (2 fails) · Locked.
| Current state | Event | Next state |
|---|---|---|
| Active | Successful login | Active (logged in) |
| Active | Failed login | Active (1 fail) |
| Active (1 fail) | Failed login | Active (2 fails) |
| Active (1 fail) | Successful login | Active (counter reset) |
| Active (2 fails) | Failed login | Locked |
| Active (2 fails) | Successful login | Active (counter reset) |
| Locked | Any login attempt | Locked — "Account locked" message |
| Locked | 30 minutes elapse | Active (counter reset) |
| Locked | Password reset completed | Active (counter reset) |
Now test the invalid transitions too — these are where the real bugs are:
- Does a successful login on the third attempt reset the counter, or does the lock happen first?
- Is the counter per-account or per-IP? Two people on the same office network shouldn't lock each other out.
- Does the counter reset survive a server restart, or does a deploy silently unlock everyone?
- Does the 30-minute timer restart if a locked account is attempted again?
- Can a password reset be triggered while locked?
State transition testing is the technique that finds the questions nobody wrote down.
6. Error guessing¶
An explicitly experience-based technique: use what you know about how software breaks. Not a substitute for systematic techniques — a supplement after them.
The standard list of things worth trying on almost any input:
- Empty, whitespace-only, and
null - Zero, negative numbers, very large numbers, leading zeros (
007) - Special characters:
' " < > & % \ / ; -- $ { } - Unicode, emoji, right-to-left text, combining accents
- Very long strings (10,000 characters)
- SQL fragments (
' OR '1'='1) and script tags (<script>alert(1)</script>) - Duplicate submission — double-click the submit button
- Back button after submit; refresh after submit; open the same form in two tabs
- Session expiry mid-form
- Network interruption mid-request
- Copy-paste with hidden formatting from Word
- Values in the wrong format that look right (
31/02/2026,2026-13-01)
7. Exploratory testing¶
Scripted testing checks what you already thought of. Exploratory testing is simultaneous learning, test design and execution — you use what you just learned to decide what to try next. It is not "random clicking"; it's structured improvisation.
Session-based test management¶
The discipline that makes exploratory testing accountable:
- Write a charter — a one-sentence mission for a timeboxed session:
"Explore the coupon-application flow with mixed carts, using expired, case-varied and stacked codes, to discover inconsistencies between the cart total and the order confirmation."
- Timebox it. 60 or 90 minutes. Longer and focus decays.
- Take notes as you go — what you tried, what you observed, questions raised, bugs found, and any setup time (which is data about testability).
- Debrief — 10 minutes with a lead or peer: what did you find, what didn't you get to, what's the next charter?
A session sheet¶
| Field | Content |
|---|---|
| Charter | Explore coupon application on mixed carts |
| Area | Cart / Checkout |
| Build | 2.14.0-rc3 |
| Duration | 90 min (60 test design/execution · 20 bug investigation · 10 setup) |
| Data used | SAVE20, EXPIRED10, save20, SAVE20, carts of 1/2/5 items |
| Bugs | BUG-118, BUG-121 |
| Issues | Staging seed data lacks sale-flagged items — had to create one manually |
| Questions | Should a code survive removing the item that qualified the cart? |
| Next charter | Coupon behaviour when the cart is modified after application |
Heuristics to drive a session¶
Structured prompts stop exploration turning into aimless clicking:
- Vary the sequence. Apply the coupon before adding items, not after.
- Interrupt. Navigate away mid-flow, then return. Refresh. Use the back button.
- Go to the extremes. Zero items, one item, fifty items.
- Break the assumption of one user. Same account in two browsers, both changing the cart.
- Follow the data. Does what the cart shows match the order email, the order history page, and the admin view?
- CRUD each entity. Create, read, update, delete — and delete something another screen still references.
- Test the "and then?" — after the successful path, what state is left behind?
Exploratory + scripted is not either/or
A mature process runs scripted regression (predictable, automatable coverage) and exploratory sessions (finds the unknown unknowns). Organisations that drop exploratory testing because "everything is automated" ship bugs that no script was ever going to look for.
8. Choosing a technique¶
| Situation | Technique |
|---|---|
| Input field with a range or set of valid values | Equivalence partitioning + BVA |
Any limit, threshold, or </<= decision |
BVA |
| Output depends on a combination of conditions | Decision table |
| Behaviour depends on prior events / status | State transition |
| A brand-new feature you barely understand | Exploratory session |
| A stable feature about to be released | Scripted regression |
| Time is short and risk is unclear | Risk-based prioritisation + exploratory |
| Requirement seems complete but feels wrong | Error guessing + exploratory |
How It Actually Works¶
Exploratory testing looks unstructured, but it's doing something specific under the hood: building a mental model of the program's actual state machine and hunting for transitions the spec never described.
Every UI you test is backed by real program state — variables, session data, DOM nodes — and every action you take (click, type, navigate) is an event that mutates that state through a handler function. A scripted test case only ever drives a small, pre-planned set of these transitions. Exploratory testing's value comes from covering transitions no one wrote down: what happens if you trigger the "submit" handler twice before the first request's response returns (a race between two async callbacks mutating the same state), or navigate back mid-submission (an event handler firing against a component that's already been torn down). These are exactly the classes of defects that show up as flaky tests in Level 3 — they're timing- and state-ordering-dependent, which is also why they're hard to write a deterministic scripted test case for in the first place.
Session-based test management's timer and charter aren't bureaucracy — they bound an otherwise-unbounded state space. The reachable-states graph of a real application is often too large to enumerate exhaustively (this is the same combinatorial explosion that limits exhaustive test-case generation for any nontrivial program), so a charter is a deliberate, human-directed traversal strategy through that graph, prioritizing the paths most likely to hide a defect (recently changed code, complex conditionals, areas with a history of bugs) over uniform random exploration.
Exercise¶
For an online hotel-booking form (check-in date, check-out date, number of guests 1–8, number of rooms 1–4, promo code, loyalty tier Bronze/Silver/Gold):
- Write the equivalence partitions for number of guests and for promo code, with one representative value each and a note on which partitions the requirement fails to define.
- Do a full BVA on both number of guests and number of rooms, plus a date-related BVA set covering: check-in today, check-in yesterday, check-out equal to check-in, check-out before check-in, a stay spanning 29 February 2028, and a stay spanning a year boundary.
- Build a decision table for this rule and derive one test case per rule with concrete data:
Free breakfast is included for Gold members on any stay; for Silver members on stays of 3 nights or more; and for anyone booking 2 or more rooms. Bronze members on a 1-night single-room stay pay for breakfast.
- Draw the state transitions for a booking:
Draft → Confirmed → Checked-in → Checked-out, plusCancelledandNo-show. List five invalid transitions you would explicitly test, and what should happen for each. - Write three exploratory charters for this booking flow, each timeboxed at 60 minutes and each targeting a different risk. Then run one of them against any real booking site and complete a session sheet using the format in section 7.