E2E Testing Strategy: Covering All Nodes and Branches in Your User Journeys
Introduction
A Sydney fintech startup thought they had comprehensive testing—until a customer discovered they could transfer money to an account that didn’t exist. The bug? Their tests covered the happy path (valid account number) but missed the branch where the validation check could fail silently.
This is the problem with incomplete test coverage: you think you’re testing everything, but you’re actually only testing the paths you remember to test. A proper E2E (end-to-end) testing strategy systematically identifies every node (decision point) and every branch (possible path) in your user journeys.
For Australian SMBs building custom software, incomplete testing isn’t just a technical issue—it’s a business risk. This guide shows you how to achieve complete test coverage by mapping every possible path through your application.
Understanding Nodes and Branches
What is a Node?
A node is any point in your user journey where a decision is made or validation occurs. Think of it as a fork in the road.
Examples of nodes:
Login validation → User exists? (YES/NO)
Payment processing → Card valid? (YES/NO)
Form submission → Data complete? (YES/NO)
API call → Service available? (YES/NO)
What is a Branch?
A branch is a specific path that extends from a node. Each node has at least two branches (usually success and failure), but complex nodes can have many more.
Example: Email validation node
Email Validation Node
├─ Branch 1: Valid format AND domain exists → SUCCESS
├─ Branch 2: Invalid format → ERROR ("Invalid email format")
├─ Branch 3: Valid format BUT domain doesn't exist → ERROR ("Domain not found")
├─ Branch 4: Valid format BUT disposable email → ERROR ("Disposable emails not allowed")
└─ Branch 5: Empty field → ERROR ("Email required")
The Problem: Missing Branches
Common mistake:
// Inadequate test - only tests Branch 1
test('should accept valid email', () => {
expect(validateEmail('[email protected]')).toBe(true);
});
Complete testing:
// Tests all 5 branches
test('should accept valid email with existing domain', () => {
expect(validateEmail('[email protected]')).toBe(true);
});
test('should reject invalid email format', () => {
expect(validateEmail('invalid-email')).toBe(false);
expect(getError()).toBe('Invalid email format');
});
test('should reject non-existent domain', () => {
expect(validateEmail('user@thisdoma indoesntexist12345.com')).toBe(false);
expect(getError()).toBe('Domain not found');
});
test('should reject disposable emails', () => {
expect(validateEmail('[email protected]')).toBe(false);
expect(getError()).toBe('Disposable emails not allowed');
});
test('should reject empty email', () => {
expect(validateEmail('')).toBe(false);
expect(getError()).toBe('Email required');
});
Step 1: Map All Decision Nodes
Start by identifying every point in your user journey where a decision is made or data is validated.
Systematic Node Discovery
For each screen/page:
- List all user inputs (text fields, buttons, dropdowns)
- List all data validations (client-side and server-side)
- List all API calls
- List all state transitions
- List all conditional rendering
Example: Australian SMB order checkout
| Screen | Node | Decision Criteria |
|---|---|---|
| Cart | Proceed to checkout | Cart not empty? Minimum order met? |
| Shipping | Address validation | Valid AU address? Delivery available? |
| Shipping | Delivery date | Date available? Not public holiday? |
| Payment | Card validation | Valid number? Expiry not past? CVV correct? |
| Payment | Payment processing | Sufficient funds? Card not blocked? Network OK? |
| Confirmation | Order creation | Database write succeeds? |
| Confirmation | Email notification | Email service available? Valid email? |

Hidden Nodes (Often Missed)
Session/authentication nodes:
- Is user still logged in?
- Has session expired during checkout?
- Is user authorized for this action?
Network nodes:
- Is API reachable?
- Did request timeout?
- Is service rate-limited?
Database nodes:
- Did write succeed?
- Did transaction commit?
- Was there a duplicate key error?
Third-party service nodes:
- Is payment gateway responding?
- Is email service available?
- Is SMS provider working?
Australian-specific nodes:
- Is postcode valid (4 digits, exists in AU)?
- Is ABN valid (11 digits, checksum correct)?
- Is phone number valid (+61 or 04 format)?
- Is state/territory valid (NSW/VIC/QLD/WA/SA/TAS/NT/ACT)?
Step 2: Identify All Branches per Node
For each node, systematically list every possible outcome.
The Branch Discovery Framework
For any validation or decision node, consider:
- Success case: Expected valid input/state
- Format failures: Wrong data type/format
- Business logic failures: Valid format, invalid for business rules
- Boundary conditions: Min/max values, edge of limits
- System failures: External service down, timeout, error
- Security issues: XSS attempts, SQL injection, unauthorized access
Example: Australian Phone Number Validation
Node: Validate phone number for SMS notifications
Branches:
// Branch 1: Valid mobile with 04 prefix
'+61412345678' → SUCCESS
'0412345678' → SUCCESS
// Branch 2: Valid mobile with spaces (normalize)
'04 1234 5678' → SUCCESS (normalized to 0412345678)
// Branch 3: Invalid - too short
'0412345' → ERROR 'Invalid phone number length'
// Branch 4: Invalid - too long
'04123456789999' → ERROR 'Invalid phone number length'
// Branch 5: Invalid - not mobile (landline)
'0298765432' → ERROR 'Must be mobile number for SMS'
// Branch 6: Invalid - contains letters
'041234ABCD' → ERROR 'Phone number must contain only digits'
// Branch 7: Invalid - international (not AU)
'+1234567890' → ERROR 'Must be Australian mobile number'
// Branch 8: Empty field
'' → ERROR 'Phone number required'
// Branch 9: Null/undefined
null → ERROR 'Phone number required'
// Branch 10: Special characters
'+61-412-345-678' → SUCCESS (normalized)
// Branch 11: SQL injection attempt (security)
"'; DROP TABLE users; --" → ERROR 'Invalid characters'
That’s 11 branches for a simple phone validation. Most teams test only Branch 1.
Step 3: Create a Branch Coverage Matrix
Document every node and its branches in a structured format.
Template:
| Feature | Node ID | Node Description | Branch ID | Branch Description | Expected Outcome | Test Status | Priority |
|---|---|---|---|---|---|---|---|
| Checkout | CHK-01 | Cart validation | CHK-01-A | Cart has items | Proceed to shipping | ✅ Tested | P0 |
| Checkout | CHK-01 | Cart validation | CHK-01-B | Cart empty | Show error | ✅ Tested | P0 |
| Checkout | CHK-01 | Cart validation | CHK-01-C | Minimum order not met | Show error + amount needed | ✅ Tested | P1 |
| Checkout | CHK-01 | Cart validation | CHK-01-D | Item out of stock | Remove item + notify user | ❌ Not tested | P0 |
| Checkout | CHK-02 | Address validation | CHK-02-A | Valid AU address | Accept address | ✅ Tested | P0 |
| Checkout | CHK-02 | Address validation | CHK-02-B | Invalid postcode | Show error | ✅ Tested | P0 |
| Checkout | CHK-02 | Address validation | CHK-02-C | Postcode out of delivery range | Show “unavailable” error | ❌ Not tested | P1 |
Priority Levels
P0 (Critical): Must test before release
- Happy path (most common flow)
- Security vulnerabilities
- Data corruption risks
- Payment/financial transactions
P1 (High): Should test before release
- Error messages
- Edge cases that affect user experience
- Alternative valid paths
P2 (Medium): Test when time permits
- Rare edge cases
- Cosmetic issues
- Nice-to-have validations
P3 (Low): Optional
- Extreme edge cases
- Situations that require deliberate user error
Step 4: Calculate Test Coverage
Formula:
Test Coverage = (Tested Branches / Total Branches) × 100%
Example:
- Total identified branches: 87
- Tested branches: 52
- Coverage: 59.8%
Industry benchmarks:
- Startup MVP: 60-70% (focus on P0 and P1)
- Production app: 80-90% (include P0, P1, and most P2)
- Critical systems (finance, healthcare): 95%+ (all P0, P1, P2)
What Percentage Should You Aim For?
For Australian SMBs:
| Business Type | Recommended Coverage | Rationale |
|---|---|---|
| E-commerce | 85% | Payment and order processing are critical |
| SaaS B2B | 80% | Business continuity for customers |
| Internal tools | 70% | Lower risk, faster iteration |
| Healthcare/Finance | 95% | Regulatory requirements, data sensitivity |
| Marketing websites | 60% | Low business impact from bugs |
Step 5: Implement Tests for Each Branch
Organize Tests by Node
// tests/checkout/cart-validation.test.ts
describe('Node CHK-01: Cart Validation', () => {
test('Branch CHK-01-A: Cart has items - should proceed', async () => {
// Setup: Add items to cart
await addToCart('product-123', 2);
// Action: Proceed to checkout
await clickCheckout();
// Assert: Redirects to shipping
expect(currentPage()).toBe('/checkout/shipping');
});
test('Branch CHK-01-B: Cart empty - should show error', async () => {
// Setup: Ensure cart is empty
await clearCart();
// Action: Try to checkout
await clickCheckout();
// Assert: Error shown, stays on cart page
expect(getErrorMessage()).toBe('Your cart is empty');
expect(currentPage()).toBe('/cart');
});
test('Branch CHK-01-C: Minimum order not met - should show amount needed', async () => {
// Setup: Add item worth $35 (minimum is $50)
await addToCart('product-cheap', 1); // $35
// Action: Proceed to checkout
await clickCheckout();
// Assert: Shows how much more needed
expect(getErrorMessage()).toContain('Minimum order is $50.00');
expect(getErrorMessage()).toContain('Add $15.00 more');
});
test('Branch CHK-01-D: Item out of stock - should remove and notify', async () => {
// Setup: Add item, then simulate going out of stock
await addToCart('product-limited', 1);
await simulateOutOfStock('product-limited');
// Action: Proceed to checkout
await clickCheckout();
// Assert: Item removed, user notified
expect(getNotification()).toContain('Salmon Sushi Roll is no longer available');
expect(cartContains('product-limited')).toBe(false);
});
});
Test Naming Convention
Use descriptive names that map to your branch matrix:
test('[Branch ID]: [Scenario] - [Expected outcome]')
Examples:
test('CHK-01-A: Valid cart with items - should proceed to shipping')
test('CHK-02-C: Postcode outside delivery range - should show unavailable error')
test('PAY-03-B: Card declined insufficient funds - should show specific error')
This makes it easy to track which branches are tested in your coverage matrix.
Step 6: Handle Complex Multi-Node Paths
Some user journeys involve multiple decision nodes in sequence. You need to test combinations of branches.
Path Combination Strategy
Example: Checkout flow has 4 nodes
Node 1: Cart validation (3 branches)
Node 2: Address validation (4 branches)
Node 3: Payment method selection (2 branches)
Node 4: Payment processing (5 branches)
Total possible paths: 3 × 4 × 2 × 5 = 120 paths
Testing all 120 is impractical. Use risk-based path selection:
- Test the happy path (most common combination)
- Test critical error paths (payment failure, validation errors)
- Test boundary combinations (minimum order + cheapest shipping)
- Test one failure at each node (isolate node behavior)
Example test selection:
// Path 1: Happy path (all branches succeed)
test('Complete checkout with valid cart, address, card', async () => {
await addToCart('product-123');
await enterAddress('123 George St, Sydney, NSW, 2000');
await selectPaymentMethod('card');
await enterCardDetails('4242424242424242', '12/25', '123');
await submitOrder();
expect(orderConfirmation()).toBeVisible();
});
// Path 2: Cart valid, address invalid
test('Cart valid but address outside delivery range', async () => {
await addToCart('product-123');
await enterAddress('123 Main St, Perth, WA, 6000'); // Out of range
expect(getError()).toContain('delivery not available');
});
// Path 3: Cart valid, address valid, payment fails
test('Valid cart and address but card declined', async () => {
await addToCart('product-123');
await enterAddress('123 George St, Sydney, NSW, 2000');
await selectPaymentMethod('card');
await enterCardDetails('4000000000000002', '12/25', '123'); // Test card that declines
await submitOrder();
expect(getError()).toContain('card was declined');
expect(orderNotCreated()).toBe(true); // Critical: No order if payment fails
});
Step 7: Test Edge Cases Systematically
Edge cases are branches at the boundary of valid behavior.
Edge Case Checklist
For numeric inputs:
- Minimum value
- Maximum value
- Zero
- Negative (if applicable)
- Decimal precision (e.g., $10.999)
- Very large numbers
- Non-numeric characters
For string inputs:
- Empty string
- Single character
- Maximum length
- Maximum length + 1
- Special characters (quotes, angle brackets, ampersand)
- Unicode/emoji
- SQL injection attempts
- XSS attempts (script tags)
For dates:
- Today
- Yesterday (past date)
- Tomorrow (future date)
- Leap year date (Feb 29)
- Invalid date (Feb 31)
- Public holidays (for Australian apps)
- Weekends
- EOFY (June 30 for Australian businesses)
For Australian-specific data:
- Postcodes: 0000, 9999, 0200 (Canberra), 0800 (NT)
- ABN: 11 digits, valid checksum
- Phone: +61 vs 04 prefix, spaces, hyphens
- State: All 8 states/territories
- Currency: Correct rounding for cents
Real Example: Order Quantity Edge Cases
describe('Product quantity edge cases', () => {
test('Minimum quantity (1) - should accept', async () => {
await setQuantity(1);
expect(addToCartButton()).toBeEnabled();
});
test('Zero quantity - should show error', async () => {
await setQuantity(0);
expect(getError()).toBe('Quantity must be at least 1');
});
test('Negative quantity - should reject', async () => {
await setQuantity(-5);
expect(getError()).toBe('Quantity must be positive');
});
test('Maximum available stock (exact) - should accept', async () => {
// Product has 50 in stock
await setQuantity(50);
expect(addToCartButton()).toBeEnabled();
});
test('Exceed available stock (50+1) - should show error', async () => {
await setQuantity(51);
expect(getError()).toContain('Only 50 available');
});
test('Decimal quantity - should reject', async () => {
await setQuantity(2.5);
expect(getError()).toBe('Quantity must be a whole number');
});
test('Very large quantity (999999) - should handle gracefully', async () => {
await setQuantity(999999);
expect(getError()).toContain('Maximum quantity is');
});
test('Non-numeric input - should reject', async () => {
await setQuantity('abc');
expect(getError()).toBe('Please enter a valid number');
});
});
Step 8: Track and Maintain Coverage
Create a Living Document
Your branch coverage matrix should be updated when:
- New features are added
- Requirements change
- Bugs are found (new branches discovered)
- Code is refactored
Version control your matrix:
/docs
/test-coverage
- checkout-flow-coverage.xlsx
- user-registration-coverage.xlsx
- payment-processing-coverage.xlsx
Automated Coverage Reporting
Use code coverage tools to supplement manual tracking:
# Run tests with coverage
npm test -- --coverage
# Generate HTML report
open coverage/index.html
Coverage metrics to track:
- Line coverage: % of code lines executed
- Branch coverage: % of if/else branches taken
- Function coverage: % of functions called
Important: 100% code coverage ≠ 100% branch testing. Code coverage tools only tell you which code ran, not whether all logical branches were tested with correct assertions.
Real-World Case Study: Brisbane Property Manager
Business: Property management system for 200+ rental properties
Challenge: Recurring bugs in tenant maintenance request processing
Solution: Implemented comprehensive branch coverage
Before Branch Coverage Strategy
Nodes identified: 8 Branches identified: 12 Test coverage: 41% Bugs in first 3 months: 23
Typical issues:
- Emergency requests not triggering immediate notifications
- Photos over 5MB crashing upload
- Multiple submissions creating duplicate requests
- Tenants from suspended accounts could submit requests
After Branch Coverage Strategy
Nodes identified: 8 (same feature, better analysis) Branches identified: 47 (nearly 4× more!) Test coverage: 89% Bugs in first 3 months: 3
Time investment:
- Mapping nodes/branches: 6 hours
- Writing new tests: 12 hours
- Total: 18 hours
Time saved:
- Prevented production bugs: ~40 hours debugging/fixing
- Reduced support tickets: ~15 hours
- ROI: 55 hours saved - 18 hours invested = 37 hours net savings
Common Mistakes and How to Avoid Them
Mistake 1: Testing Only the Happy Path
Problem: 80% of bugs occur in error branches
Solution: For every success test, write at least one failure test
Mistake 2: Assuming “It Will Never Happen”
Problem: Users find edge cases you never imagined
Solution: Test edge cases even if they seem unlikely
Real example: Perth software company assumed no one would enter a birthdate in the future. First week, someone entered “2050” as their birth year (testing the system) and crashed the age calculation logic.
Mistake 3: Not Testing External Service Failures
Problem: External APIs (payment, email, SMS) can fail
Solution: Mock external services and test failure scenarios
test('Should handle Stripe API timeout gracefully', async () => {
// Mock Stripe to timeout
mockStripeTimeout();
await submitPayment();
// Should show retry option, not crash
expect(getError()).toContain('connection issue');
expect(retryButton()).toBeVisible();
});
Mistake 4: Ignoring Australian Compliance
Problem: Generic tests miss local requirements
Solution: Specific tests for Australian business rules
test('Should calculate GST correctly on $99.90 order', async () => {
const subtotal = 99.90;
const gst = calculateGST(subtotal);
expect(gst).toBe(9.08); // $99.90 × 10 / 110 = $9.08 (rounded)
expect(subtotal + gst).toBe(109.08);
});
Conclusion
Complete E2E test coverage isn’t about testing everything—it’s about systematically identifying and testing every node and branch in your user journeys. For Australian SMBs, this approach transforms testing from guesswork to science.
Key takeaways:
- Map all decision nodes in your user flows
- Identify all branches (outcomes) for each node
- Prioritize branches by risk (P0/P1/P2/P3)
- Test critical paths and error cases
- Track coverage in a living document
- Update as requirements change
The investment pays off in fewer production bugs, faster debugging, and more confident deployments.
Need help implementing comprehensive E2E testing for your Australian business software? We’ve helped 40+ SMBs achieve 85%+ test coverage. Contact us for a free testing audit.