Skip to Content
Course content

130: Property-Based Testing Concepts in Java

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of us have a habit of writing tests based on the examples we already have in our heads. We think, "If I pass 'Hello World' into my slug generator, it should return 'hello-world'." We write that test, it passes, and we move on. I've spent a fair amount of my career shipping code that passed every single single-case test I wrote, only to have it crash in production because a user entered a string containing a newline character or a non-breaking space.

public String toSlug(String input) {
    // A simple implementation to turn a title into a URL-friendly slug
    return input.toLowerCase().trim().replaceAll("\\s+", "-");
}

The Illusion of Coverage

If I were testing this the traditional way, I'd probably write three or four tests: a standard string, a string with multiple spaces, and maybe an empty string. They would all pass. I'd feel confident. But here is the problem: we aren't testing the logic; we're testing our own ability to guess where the code might break. We are missing the "unknown unknowns."

What happens if the input is null? What happens if the string contains emojis, punctuation, or control characters? In the code above, a null input triggers a NullPointerException immediately. A string like "Hello World!!!" becomes "hello-world!!!", which isn't actually a clean URL slug. My examples were too "happy."

Letting the Machine Find the Edge Cases

This is where Property-Based Testing (PBT) changes the game. Instead of picking specific inputs, we define a property—a rule that must hold true for any possible input. For our slug generator, a property might be: "The output should never contain spaces, regardless of the input string."

In Java, using a library like jqwik, we don't write a @Test; we write a @Property. We tell the framework, "Generate a thousand random strings, including the weird ones, and try to break this rule."

@Property
void slugShouldNeverContainSpaces(@ForAll String input) {
    String result = toSlug(input);
    Assertions.assertFalse(result.contains(" "), "Slug should not contain spaces");
}

The moment I run this, the framework doesn't just give me a "Pass." It hammers the method with nulls, tabs, carriage returns, and Unicode whitespace. It will find that null input in milliseconds and fail the test. It forces me to stop thinking about "examples" and start thinking about "invariants."

Hardening the Slug Generator

To fix this, I need to stop relying on a simple regex and start handling the actual constraints of the property. I need to ensure the input is handled safely and that any character that isn't a lowercase letter, a number, or a hyphen is stripped out.

public String toSlug(String input) {
    if (input == null) {
        return "";
    }
    
    String normalized = input.toLowerCase().trim();
    // Replace all non-alphanumeric characters with hyphens
    String slug = normalized.replaceAll("[^a-z0-9]+", "-");
    
    // Remove leading or trailing hyphens that might have been created
    return slug.replaceAll("^-+|-+$", "");
}

Now, when the PBT tool runs a thousand random strings—including strings of just punctuation or strings with mixed whitespace—the property "should never contain spaces" holds true. More importantly, the property "should only contain lowercase letters, numbers, and hyphens" also holds true.

The Power of Shrinking

One thing I love about PBT that you won't get with random fuzzing is shrinking. If the tool finds a massive, 500-character string that breaks your code, it doesn't just hand you that giant mess. It automatically tries to find the smallest possible input that still causes the failure.

If a string with a weird combination of a Tab, a Newline, and a Cyrillic character breaks your logic, the tool will "shrink" it down. It might tell you, "I found a failure with this 500-character string, but the simplest version that still fails is actually just a single \t (tab)." This saves you from spending an hour debugging a massive input string to figure out which specific character caused the crash.




📋 Practical Task

Implement a Robust Symmetric Array Shuffler

You are tasked with writing a method that shuffles an array of integers. While "randomness" is hard to test, a key property of a shuffle is that the resulting array must contain exactly the same elements as the original array, just in a different order (this is called a permutation).

Your Task:

  • Create a class ArrayUtils with a method public int[] shuffle(int[] input).
  • Write a property-based test (conceptually or using a framework like jqwik) that asserts the following property: "For any integer array, the sorted version of the shuffled array must be identical to the sorted version of the original array."
  • Ensure your shuffle method handles null inputs and empty arrays without crashing.
  • Verify that your implementation doesn't accidentally lose or duplicate elements during the shuffle process.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.