Equivalence Partitioning for Non-Numeric Types: Strings, Dates, Enums, and Collections
Most equivalence partitioning (EP) tutorials use age ranges and temperature thresholds. Clean, numeric, easy to understand. The real world is messier: username validation, date ranges with timezone edge cases, enum state machines, and variable-length collections.
This post covers EP for non-numeric input types where the partition boundaries aren't as obvious as "1 to 100."
Review: What EP Is Solving
EP assumes that if a system handles one value in a partition correctly, it handles all values in that partition correctly. This lets you test one representative from each partition instead of every possible value.
The skill is identifying the right partitions. For numeric ranges, partition boundaries are explicit. For other types, you have to derive them from system behavior.
Equivalence Partitioning for Strings
Strings are the most complex input type for EP because they have multiple dimensions: length, character content, encoding, and format.
Length partitions
For a username field (3–20 characters):
| Partition | Range | Representative |
|---|---|---|
| Empty | 0 chars | "" |
| Too short | 1–2 chars | "ab" |
| Valid minimum | 3 chars | "abc" |
| Valid typical | 4–19 chars | "johndoe" |
| Valid maximum | 20 chars | "abcdefghijklmnopqrst" |
| Too long | 21+ chars | "abcdefghijklmnopqrstu" |
These are the same length partitions you'd use for any input — but they only cover one dimension of string validation.
Character type partitions
Most string fields have character restrictions. A username might allow alphanumeric + underscore but reject everything else:
| Partition | Content | Representative |
|---|---|---|
| All lowercase | a-z only | "johndoe" |
| All uppercase | A-Z only | "JOHNDOE" |
| Mixed case | Both | "JohnDoe" |
| Numbers only | 0-9 | "12345678" |
| Special chars (valid) | Underscore | "john_doe" |
| Special chars (invalid) | Other symbols | "john@doe" |
| Unicode letters | Latin extended | "Jöhn" |
| Unicode symbols | Emoji, CJK | "john😀" |
| Whitespace | Space, tab, newline | "john doe" |
| Null byte | \x00 | "john\x00doe" |
Each character category can behave differently. Null bytes and control characters often bypass string length checks and break downstream processing. Unicode characters may bypass character-allowlist checks.
Format-specific partitions
For email addresses:
| Partition | Representative | Notes |
|---|---|---|
| Valid simple | "user@example.com" | Baseline valid |
| Valid complex | "user.name+tag@sub.example.co.uk" | Subdomains, plus-addressing |
| No @ symbol | "userexample.com" | Missing required character |
| Multiple @ | "user@@example.com" | Invalid format |
| No domain | "user@" | Missing domain |
| No local part | "@example.com" | Missing local part |
| Internationalized | "üser@example.com" | IDN email |
| Very long local | "a" × 65 + "@example.com" | RFC 5321 limit is 64 chars |
| Very long domain | "user@" + "a" × 256 + ".com" | DNS name limit |
| IP address domain | "user@[192.168.1.1]" | Valid per RFC, often blocked |
For phone numbers, passwords, URLs, credit card numbers — each format has its own partition structure derived from the format specification and common validation failure modes.
SQL injection and XSS partitions
For any string that reaches a database or HTML output:
| Partition | Representative |
|---|---|
| SQL keywords | "'; DROP TABLE users; --" |
| SQL quote escaping | "O'Brien" |
| XSS payload | "" |
| HTML entities | "<script>" |
| Unicode bypasses | "<script>" (fullwidth) |
These partitions aren't about normal valid/invalid — they're security-relevant partitions that verify the system handles potentially malicious input without executing it.
Equivalence Partitioning for Dates and Times
Date EP is complex because of calendar irregularities and time zones.
Date value partitions
For an event date field (must be future date):
| Partition | Representative |
|---|---|
| Well in the past | "2000-01-01" |
| Yesterday | today - 1 day |
| Today | today |
| Tomorrow | today + 1 day |
| Far future | "2099-12-31" |
The partitions relative to "today" require test setup that pins the current date, or test design that accommodates a moving boundary.
Calendar edge cases as partitions
| Partition | Representative | Why It Matters |
|---|---|---|
| February 28 non-leap year | "2023-02-28" | Last day of month |
| February 29 leap year | "2024-02-29" | Only valid in leap years |
| February 30 | "2023-02-30" | Never valid |
| Last day of month | "2023-01-31" | 31-day month boundary |
| Last day of year | "2023-12-31" | Year rollover |
| First day of year | "2024-01-01" | Year rollover |
Many date parsing bugs concentrate at month/year boundaries. February 29 is the classic example: code that fails to account for leap years produces wrong results once every four years.
Timezone partitions
For applications with timezone-aware dates:
| Partition | Representative | Why It Matters |
|---|---|---|
| UTC | "2023-01-15T12:00:00Z" | Baseline |
| Positive offset | "2023-01-15T12:00:00+05:30" | IST |
| Negative offset | "2023-01-15T12:00:00-08:00" | PST |
| DST spring-forward | "2023-03-12T02:30:00-05:00" | Doesn't exist in US Eastern |
| DST fall-back | "2023-11-05T01:30:00-05:00" | Exists twice in US Eastern |
| Midnight UTC boundary | "2023-01-15T00:00:00Z" | Date depends on timezone |
Daylight saving time transitions are notorious for revealing timezone bugs. A time that "doesn't exist" (spring forward) or "exists twice" (fall back) triggers edge cases in most time handling libraries.
Equivalence Partitioning for Enumerations
Enums have a different partition structure: there are no continuous ranges, only discrete valid and invalid values.
State machine partitions
For an order status enum with values: PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED:
The partitions aren't just "valid value" and "invalid value" — they're state-dependent. An order in SHIPPED state can transition to DELIVERED or CANCELLED, but not back to PENDING.
| Current State | Valid Transitions | Invalid Transitions |
|---|---|---|
| PENDING | CONFIRMED, CANCELLED | SHIPPED, DELIVERED |
| CONFIRMED | SHIPPED, CANCELLED | PENDING, DELIVERED |
| SHIPPED | DELIVERED, CANCELLED | PENDING, CONFIRMED |
| DELIVERED | (none) | All others |
| CANCELLED | (none) | All others |
EP for state machines: each valid transition is one partition (test one example). Each invalid transition from each state is another partition (test one example of each illegal transition).
For an order, that's at minimum 7 valid transition tests and ~15 invalid transition tests (varies by state machine shape).
Unknown/garbage enum values
Beyond valid enum values, test:
- Null/undefined
- Empty string
- Unrecognized string values ("REFUNDED", "VOID")
- Numeric values if the enum maps to integers
- Out-of-range numeric values
APIs that serialize enums as strings often fail when receiving unrecognized values. The partition "unrecognized enum value" should be explicitly tested.
Equivalence Partitioning for Collections (Lists, Arrays)
Collections have size as a primary partition dimension and element validation as a secondary dimension.
Size partitions
For a cart with 0–50 items:
| Partition | Count | Representative |
|---|---|---|
| Empty | 0 | [] |
| Single item | 1 | [item] |
| Multiple items (typical) | 2–49 | [item, item, item] |
| Maximum | 50 | [item × 50] |
| Over maximum | 51+ | [item × 51] |
Empty and single-element collections frequently reveal bugs because:
- Empty: code assumes at least one element (null pointer, division by zero when computing average)
- Single: code assumes multiple elements (min/max comparison logic fails with one element)
Element validity partitions
For a list that should contain only positive integers:
| Partition | Representative |
|---|---|
| All valid | [1, 2, 3] |
| Contains zero | [1, 0, 3] |
| Contains negative | [1, -1, 3] |
| Contains non-integer | [1, 1.5, 3] |
| Contains null | [1, null, 3] |
| Contains duplicate | [1, 1, 2] |
| All same value | [5, 5, 5] |
Mixed validity (some valid, some invalid) reveals how the system handles partial validation: does it reject the whole collection, process only valid items, or return an error per invalid item?
Ordering partitions
If the collection order matters (sorted list):
| Partition | Representative |
|---|---|
| Already sorted ascending | [1, 2, 3, 4, 5] |
| Already sorted descending | [5, 4, 3, 2, 1] |
| Random order | [3, 1, 4, 1, 5] |
| Already sorted (no change) | [1, 2, 3] — test idempotency |
| Single element | [5] |
Sorting bugs often manifest at the partitions where no actual sorting is needed (already sorted, single element). These edge cases are underrepresented in tests.
Identifying Non-Obvious Partitions
For complex types, extract partitions systematically:
Step 1: Read the specification What are the explicit rules? Each rule creates at least one partition (satisfies rule) and one (violates rule).
Step 2: Model the behavior What does the system do with this input? Map the input to system behavior. Different behaviors = different partitions.
Step 3: Look for implicit boundaries What values have "interesting" behavior not documented in the spec? String length at database column limits (255, 65535), numeric values at integer overflow points, dates at calendar boundaries.
Step 4: Apply security intuition What values has the OWASP Top 10 shown to cause vulnerabilities? SQL injection strings, XSS payloads, format string attacks for string inputs.
Step 5: Consider the consumer What does the next system do with this input? A string that's valid for your validation but causes a problem in your database, your email sender, or your report generator represents a boundary you need to partition.
Summary
Equivalence partitioning works for any input type that has distinct behavioral regions. For strings: length, character type, format, and security partitions. For dates: value relative to boundaries, calendar edge cases, and timezone. For enums: state machine transitions and invalid values. For collections: size (especially empty and maximum) and element validity.
The discipline: derive partitions from behavior, not from type. Two inputs belong in the same partition if and only if the system behaves identically for both. Different behavior = different partition = needs its own test.