Definition
- Equivalence partitioning – Groups inputs into classes the system treats the same, so one representative per class gives the same confidence as testing every member.
- Boundary value analysis – Tests the exact edges of each partition where off-by-one errors and wrong comparison operators cluster.
- Decision tables – Enumerates combinations of conditions and their expected actions, exposing missing or contradictory rules in the specification.
- State transition testing – Models an entity's lifecycle states and covers each legal transition plus attempts at illegal ones.
- Pairwise testing – Covers all pairs of parameter values with far fewer cases than the full cross-product, because most defects are triggered by two-parameter interactions.
Suppose the National Bank of India accepts transfer amounts from one rupee to one hundred thousand rupees. Testing every single amount is impossible, so a smart tester groups amounts into "too low," "valid," and "too high" and tests one from each group; that is equivalence partitioning. Then the tester checks the exact edges, zero, one, one hundred thousand, one hundred thousand and one; that is boundary value analysis. When loan approval depends on several yes-or-no conditions, the tester lays out all the combinations in a grid; that is a decision table. When a loan moves through applied, approved, disbursed, and closed, the tester checks every allowed and forbidden move; that is state transition testing. And when a form has many dropdowns, the tester covers every pair of choices instead of every combination; that is pairwise testing.
Understanding the Concepts
What is equivalence partitioning?
Equivalence partitioning is the foundation of test-design techniques. You divide each input into partitions, ranges or categories that the system treats identically, and then test just one representative value from each partition. If the code handles one member of a class correctly, it almost certainly handles the rest the same way.
For a transfer amount valid between one and one hundred thousand rupees, the partitions are: below range, in range, and above range, plus invalid types such as negative or non-numeric.
// EquivalencePartitioningTest.java
package com.bank.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
public class EquivalencePartitioningTest {
private final TransferValidator validator = new TransferValidator();
// Partition 1: Below range (invalid)
@Test
void zeroAmountIsRejected() {
assertFalse(validator.isValidAmount(BigDecimal.ZERO));
}
// Partition 2: In range (valid) - one representative
@Test
void midRangeAmountIsAccepted() {
assertTrue(validator.isValidAmount(new BigDecimal("50000.00")));
}
// Partition 3: Above range (invalid)
@Test
void aboveMaximumIsRejected() {
assertFalse(validator.isValidAmount(new BigDecimal("200000.00")));
}
// Partition 4: Negative (invalid)
@Test
void negativeAmountIsRejected() {
assertFalse(validator.isValidAmount(new BigDecimal("-100.00")));
}
}
What is boundary value analysis?
Boundary value analysis complements partitioning by attacking the edges, because off-by-one errors and wrong comparison operators cluster exactly at boundaries. For the one-to-one-hundred-thousand range, you test the just-outside and just-inside values.
// BoundaryValueTest.java
package com.bank.service;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class BoundaryValueTest {
private final TransferValidator validator = new TransferValidator();
@ParameterizedTest(name = "Amount {0} -> valid={1}")
@CsvSource({
"0.00, false", // just below min
"0.99, false", // just below min
"1.00, true", // exact min boundary
"1.01, true", // just above min
"99999.99, true", // just below max
"100000.00, true", // exact max boundary
"100000.01, false", // just above max
"100001.00, false" // above max
})
void boundaryAmountsAreValidatedCorrectly(
String amount, boolean expected) {
assertEquals(expected,
validator.isValidAmount(new BigDecimal(amount)));
}
}
Boundary diagram for transfer amount:
INVALID │ VALID RANGE │ INVALID
──────────┼─────────────────────┼──────────
0.99 │ 1.00 ... 100000 │ 100000.01
│ ^ ^ │
│ min boundary max boundary
│ │
Bugs cluster HERE at the boundaries
What is a decision table?
Decision tables handle combinational logic where the output depends on several conditions. You list each condition as a row, enumerate the combinations as columns, and record the expected action. This both generates test cases and reveals missing or contradictory rules.
// DecisionTableLoanApprovalTest.java
package com.bank.service;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class DecisionTableLoanApprovalTest {
private final LoanApprovalService service = new LoanApprovalService();
// Decision table:
// Income >= 50k | Credit >= 700 | Age >= 21 | Result
// T | T | T | APPROVED
// T | T | F | REJECTED (underage)
// T | F | T | REJECTED (credit)
// F | T | T | REJECTED (income)
// F | F | T | REJECTED (income+credit)
// F | F | F | REJECTED (all fail)
@ParameterizedTest(name = "Income={0} Credit={1} Age={2} -> {3}")
@CsvSource({
"60000, 750, 25, APPROVED",
"60000, 750, 18, REJECTED",
"60000, 600, 25, REJECTED",
"30000, 750, 25, REJECTED",
"30000, 600, 25, REJECTED",
"30000, 600, 18, REJECTED",
})
void loanApprovalDecisionTable(
int income, int credit, int age, String expected) {
LoanApplication app = new LoanApplication(income, credit, age);
assertEquals(expected, service.evaluate(app).getStatus());
}
}
What is state transition testing?
State transition testing applies when an entity has a lifecycle. You model the states, the events that cause transitions, and design cases to cover each legal transition plus attempts at illegal transitions to confirm they are rejected.
// LoanStateTransitionTest.java
package com.bank.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class LoanStateTransitionTest {
@Test
void legalTransition_applied_to_approved() {
Loan loan = new Loan("LOAN-001", LoanState.APPLIED);
loan.approve();
assertEquals(LoanState.APPROVED, loan.getState());
}
@Test
void legalTransition_approved_to_disbursed() {
Loan loan = new Loan("LOAN-001", LoanState.APPROVED);
loan.disburse();
assertEquals(LoanState.DISBURSED, loan.getState());
}
@Test
void illegalTransition_applied_cannot_disburse() {
Loan loan = new Loan("LOAN-001", LoanState.APPLIED);
assertThrows(IllegalStateException.class, loan::disburse);
}
@Test
void illegalTransition_closed_cannot_disburse() {
Loan loan = new Loan("LOAN-001", LoanState.CLOSED);
assertThrows(IllegalStateException.class, loan::disburse);
}
}
What is pairwise testing?
Pairwise or combinatorial testing covers all pairs of parameter values with far fewer cases than the full cross-product. Most defects are triggered by the interaction of just two parameters, so covering all pairs catches the large majority of combination bugs.
// PairwiseTransferTest.java
package com.bank.service;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
public class PairwiseTransferTest {
// 3 parameters x 3 values each = 27 full combinations
// Pairwise: only 9 cases cover all pairs
// Generated by a pairwise tool (e.g., PICT)
@ParameterizedTest(name = "Account={0} Currency={1} Channel={2}")
@CsvSource({
"SAVINGS, INR, WEB",
"SAVINGS, USD, MOBILE",
"SAVINGS, EUR, API",
"CURRENT, INR, MOBILE",
"CURRENT, USD, API",
"CURRENT, EUR, WEB",
"NRI, INR, API",
"NRI, USD, WEB",
"NRI, EUR, MOBILE",
})
void pairwiseTransferCombinations(
String accountType, String currency, String channel) {
// Each pair of values appears at least once
// 9 cases instead of 27
TransferResult result = transferService.transfer(
accountType, currency, channel,
new java.math.BigDecimal("1000.00")
);
// Assert based on expected behavior for this combination
}
}
How do these techniques work together?
These techniques are complementary, not alternatives. You choose based on the feature's characteristics.
| Technique | Use When | Example |
|---|---|---|
| Equivalence partitioning | Input has ranges or categories | Transfer amount: below, valid, above |
| Boundary value analysis | Edges of partitions matter | 0.99, 1.00, 100000, 100000.01 |
| Decision table | Multiple conditions combine | Loan approval: income + credit + age |
| State transition | Entity has a lifecycle | Loan: applied, approved, disbursed, closed |
| Pairwise | Many parameters would explode | Account type x currency x channel |
Workflow: choosing the right technique
Feature has ranges? ──> Equivalence Partitioning
│ + Boundary Value Analysis
│
Feature has combined ──> Decision Table
conditions?
│
Feature has lifecycle ──> State Transition Testing
states?
│
Feature has many ──> Pairwise / Combinatorial
independent params?
What are common mistakes?
The first mistake is applying only one technique. Partitioning alone misses boundary bugs. Boundaries alone miss combination bugs. A good tester layers the techniques.
The second mistake is creating partitions that do not reflect how the system actually behaves. If the system treats amounts above fifty thousand differently from amounts above one hundred thousand, you need both thresholds as partition boundaries, not just one.
Wrong: One partition for "valid range" 1 to 100000
Misses: system applies different fees at 50000
Correct: Three partitions for valid range
1-49999 (standard fee)
50000-99999 (reduced fee)
100000 (maximum, special handling)
What are common interview follow-up questions?
- "How many test cases does boundary value analysis produce?" For each boundary: the boundary value, one below, and one above. For a range with two boundaries, that is six cases.
- "When would you use a decision table instead of pairwise?" When the conditions are dependent and produce different actions based on specific combinations. Pairwise is for independent parameters.
- "Can you combine these techniques?" Always. Partition first to shrink the space, apply boundaries to the edges, use a decision table for combined rules, and use pairwise if parameters remain.
Visual Overview
Equivalence Partitioning + Boundary Value Analysis
───────────────────────────────────────────────────
Transfer Amount (INR)
INVALID │ VALID RANGE │ INVALID
(below min) │ (1.00 to 100000.00) │ (above max)
────────────┼──────────────────────────┼───────────
-100 0.99 │ 1.00 50000 100000.00 │ 100000.01
^ │ ^ ^ │ ^
BVA │ BVA BVA │ BVA
│ │
EP: test │ EP: test one from │ EP: test
one value │ this partition │ one value
Decision Table: Loan Approval
─────────────────────────────
Condition │ R1 │ R2 │ R3 │ R4 │ R5 │ R6
────────────────┼────┼────┼────┼────┼────┼────
Income >= 50k │ T │ T │ T │ F │ F │ F
Credit >= 700 │ T │ T │ F │ T │ F │ F
Age >= 21 │ T │ F │ T │ T │ T │ F
────────────────┼────┼────┼────┼────┼────┼────
Action │ OK │ NO │ NO │ NO │ NO │ NO
State Transition: Loan Lifecycle
────────────────────────────────
APPLIED ──approve──> APPROVED ──disburse──> DISBURSED
│ │ │
│──reject──> REJECTED │ │
│ │──cancel──> CANCELLED │
X (cannot disburse) close─┘
│
▼
CLOSED
Key Takeaway
Test-design techniques are how I get strong coverage from a small, justified set of cases instead of either testing exhaustively, which is impossible, or guessing randomly, which misses bugs. Equivalence partitioning is my starting point: I divide each input into classes the system treats the same and test one representative per class. Boundary value analysis then attacks the edges where off-by-one errors cluster. Decision tables handle logic where several conditions combine into an action, exposing missing rules. State transition testing covers an entity's lifecycle, both legal transitions and illegal ones. And pairwise testing tames the explosion when many independent parameters would otherwise produce thousands of combinations.
The key is that these are complementary. I partition to shrink the space, apply boundaries to the edges, use a decision table for combined conditions, use state transition for lifecycles, and use pairwise when parameters would explode.
At the National Bank of India this discipline paid off directly. For the transfer amount field I replaced a sprawling ad-hoc list of amounts with a tight partition-plus-boundary set and immediately caught a defect where the maximum used less-than instead of less-than-or-equal, so exactly one hundred thousand was wrongly rejected. For loan approval I built a decision table that exposed a missing rule for the borderline-age case the spec had not defined.
Being able to name each technique, say which feature it fits, and justify my case set as deliberate coverage rather than a random pile of examples is exactly what shows an interviewer I design tests like an engineer.
Remember one sentence: partition to shrink the space, boundary to find edge bugs, decision table for condition logic, state transition for lifecycles, and pairwise for parameter explosions.
---