Property‑Based Testing: How a 10‑Minute Test Caught a Hidden Inconsistency
Property‑based testing (PBT) often looks theoretical, but it can rescue everyday projects. I spent 10 minutes on a PBT check and spotted non‑idempotent behavior in Google’s popular libphonenumber library.
Formatters
We rely on formatters — like normalize_email() or PhoneNumberFormatter.format() — to make data look the same everywhere.
In a microservices world, the same record can be saved dozens of times in one workflow, so an inconsistent formatter can quietly corrupt data and break audit trails.
How many times have we all seen code like this in the model layer of our frameworks? I’m showing both Ruby and Java because this pattern pops up across languages:
before_validation :normalize_email
def normalize_email
self.email = format_email(email)
end
or
@PrePersist
public void normalizePhone() {
this.phone = PhoneFormatter.format(phone);
}
Looks familiar, doesn’t it?
The Formatter Under Test
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
class PhoneNumberFormatter {
private static final PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
public static String format(String input) throws NumberParseException {
Phonenumber.PhoneNumber number = phoneUtil.parse(input, "US");
return phoneUtil.format(number, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
}
}
Simple, right? Parse a string, format it as a U.S. number, move on (see the libphonenumber docs for details).
Putting PBT to Work
I used JQwik—a JUnit 5‑style PBT library—to throw 1,000,000 random digit strings at the formatter. The rule was simple:
Formatting twice should give the same result
The test:
import net.jqwik.api.*;
@Property(tries = 1_000_000) // 1,000,000 trials usually finish in milliseconds
void formattingIsConsistent(@ForAll("phoneNumbers") String input) throws NumberParseException {
String once;
try {
once = PhoneNumberFormatter.format(input);
} catch (NumberParseException ignored) {
return; // skip invalid inputs
}
String twice = PhoneNumberFormatter.format(once);
assertThat(twice)
.withFailMessage("Failed for: '%s' → '%s' → '%s'", input, once, twice)
.isEqualTo(once);
}
@Provide
Arbitrary<String> phoneNumbers() {
return Arbitraries.strings().withChars("1234567890").ofMaxLength(14);
}
The Surprise
Consistency failed for input: ‘0118251144’ → ‘051-144’ → ‘051144’
Hypothesis: the formatter treats national dial codes differently on re‑parse.
On the first pass Google’s library inserts a hyphen; on the second it drops it. That one‑character drift can ripple through logs, duplicate‑detection jobs, or billing systems—and it would be hard to spot with hand‑picked test cases.
Takeaway
Ten minutes of PBT saved what could have been days of production firefighting. If a single assertion can surface issues like this, imagine what a small suite could do for your codebase!
Have you tried PBT? Share your story