Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
125: Parameterized Tests
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:
- Create a test class
DiscountServiceTest. - Implement a
@ParameterizedTestusing@CsvSourcethat takes three parameters:membershipLevel(String),spend(double), andexpectedDiscount(double). - 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).
- Use
assertEqualsto verify that theDiscountService.calculateDiscount(level, spend)method returns the correct value.
There are no comments for now.