Skip to Content
Course content

125: Parameterized Tests

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

I see this all the time in code reviews from developers who are just starting to get serious about testing: they have a piece of logic that needs to be checked against ten different inputs, so they write a single @Test method and wrap the assertion in a for loop. On the surface, it looks efficient. You've covered all your cases with five lines of code. But it's a trap.

"A for-loop is just a manual parameterized test"

Let's look at why that logic fails you in the real world. Imagine you're writing a PasswordValidator that checks if a string meets your company's security requirements. You decide to test it like this:

@Test
void testPasswordStrength() {
    String[] passwords = {"Short1!", "NoDigit!", "AllLowercase1!", "PerfectPass123!"};
    for (String pw : passwords) {
        assertTrue(validator.isValid(pw));
    }
}

Here is the problem: the moment the second password ("NoDigit!") fails, the entire test stops. JUnit marks the test as failed and exits. You have no idea if "AllLowercase1!" would have passed or failed because the execution never reached it. Even worse, your CI/CD pipeline just tells you testPasswordStrength failed. You then have to spend time debugging to figure out which specific index in the array triggered the failure. It's tedious and hides bugs.

Treating every input as a first-class citizen

JUnit 5 solves this with @ParameterizedTest. Instead of one test that runs multiple checks, it allows you to define a template for a test and then feeds it a stream of arguments. Each single input is treated as its own individual test case in your IDE's test runner. If the second input fails, the third and fourth still run, and you get a clear report of exactly which values caused the breakage.

For simple lists of inputs, we use @ValueSource. Here is how I would rewrite that password validator test:

@ParameterizedTest
@ValueSource(strings = {"Short1!", "NoDigit!", "AllLowercase1!"})
void shouldRejectInvalidPasswords(String password) {
    assertFalse(validator.isValid(password), "Password should be invalid: " + password);
}

Now, your IDE will show three separate checkmarks (or red Xs). I find this much more satisfying because it gives me a high-resolution view of my code's behavior.

Handling complex scenarios with CSV sources

Most of the time, you aren't just checking if something is true or false; you're checking if a specific input produces a specific output. This is where @CsvSource becomes your best friend. It lets you map an input to an expected result without having to build a complex custom data provider.

Let's say we have a TaxCalculator. We want to ensure that different income brackets get the correct tax rate. Instead of writing five different test methods, we do this:

@ParameterizedTest
@CsvSource({
    "10000, 0.10",
    "50000, 0.20",
    "100000, 0.30",
    "0, 0.0"
})
void calculateTaxCorrectly(double income, double expectedRate) {
    assertEquals(expectedRate, calculator.getRateFor(income));
}

I love this approach because the test method becomes a specification. Anyone can look at that @CsvSource block and instantly understand the business rules for tax brackets without hunting through the actual implementation code.




📋 Practical Task

Building a Robust Discount Calculator Validator

You are tasked with testing a DiscountService class. This service calculates a discount percentage based on a customer's membership level and their total spend. The logic is as follows:

  • BRONZE: 5% discount if spend is over $100.
  • SILVER: 10% discount if spend is over $100.
  • GOLD: 15% discount regardless of spend.
  • NONE: 0% discount regardless of spend.

Your Task:

  1. Create a test class DiscountServiceTest.
  2. Implement a @ParameterizedTest using @CsvSource that takes three parameters: membershipLevel (String), spend (double), and expectedDiscount (double).
  3. Include at least 6 test cases in your source, ensuring you cover "edge cases" (e.g., a Bronze member spending exactly $100, a Gold member spending $0, and a None member spending $1000).
  4. Use assertEquals to verify that the DiscountService.calculateDiscount(level, spend) method returns the correct value.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.