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
123: Unit Testing with JUnit 5
A few years ago, I was lead on a project for a fintech client. We had a developer—smart guy, but he had a habit of "quick fixes." He pushed a one-line change to the interest calculation logic to handle a weird rounding edge case for a single client. He checked it manually with one test case, saw it worked, and merged it. Two hours later, the CFO called me because the system had accidentally wiped out the interest accruals for every corporate account in the database. He had fixed the edge case but broke the core logic for everyone else. That's the "regression" nightmare, and it's exactly why we don't trust manual testing for core business logic.
Unit testing is your safety net. Instead of clicking through the UI or manually running the app and typing in numbers, you write a separate piece of code whose only job is to verify that a specific method in your application behaves exactly as expected. JUnit 5 is the industry standard for this in Java. It allows you to run hundreds of these checks in seconds, ensuring that when you fix a bug today, you aren't creating three more for tomorrow.
Defining Expectations with Assertions
At its heart, a unit test is just a method annotated with @Test that calls a piece of logic and then asks: "Is the result what I expected?" If the answer is yes, the test passes. If no, it fails and tells you exactly why.
Let's say we're building a DiscountService that calculates a final price based on a customer's membership tier. You don't want to manually test every tier every time you change a line of code. Here is how you'd approach it with JUnit 5:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DiscountServiceTest {
@Test
void shouldApplyGoldTierDiscount() {
DiscountService service = new DiscountService();
double result = service.calculatePrice(100.0, "GOLD");
// We expect a 20% discount, so 100.0 becomes 80.0
assertEquals(80.0, result, "Gold tier should receive a 20% discount");
}
@Test
void shouldApplyNoDiscountForStandardTier() {
DiscountService service = new DiscountService();
double result = service.calculatePrice(100.0, "STANDARD");
assertEquals(100.0, result);
}
}
Notice the assertEquals method. I always recommend adding a custom message (the third argument) to your assertions. When a build fails in a CI/CD pipeline at 3 AM, seeing "Gold tier should receive a 20% discount" is infinitely more helpful than seeing "Expected 80.0 but was 100.0".
Testing for the "Sad Path"
Beginners often only test the "Happy Path"—the scenarios where everything goes right. But as a professional, you spend a lot of your time testing the "Sad Path." What happens if the user passes a null tier? What happens if the price is negative? Your code should fail predictably, and your tests should prove that it does.
JUnit 5 provides assertThrows for this. If your DiscountService is designed to throw an IllegalArgumentException when a negative price is provided, you test it like this:
@Test
void shouldThrowExceptionForNegativePrice() {
DiscountService service = new DiscountService();
assertThrows(IllegalArgumentException.class, () -> {
service.calculatePrice(-10.0, "GOLD");
});
}
I can't stress this enough: testing your exceptions is just as important as testing your return values. It's the difference between a system that crashes with a cryptic NullPointerException and one that tells the user exactly what they did wrong.
Cleaning Up the Test Noise
As you write more tests, you'll notice you're doing the same setup over and over—like instantiating the DiscountService in every single method. It makes the tests cluttered and harder to maintain. If you change the constructor of your service, you'll have to update twenty different tests. That's a waste of your time.
To fix this, we use @BeforeEach. This tells JUnit to run a specific method before every single @Test method in the class. It keeps your individual tests lean and focused on the actual logic being verified.
class DiscountServiceTest {
private DiscountService service;
@BeforeEach
void setUp() {
// This runs before every test, giving us a fresh instance
service = new DiscountService();
}
@Test
void testGoldDiscount() {
assertEquals(80.0, service.calculatePrice(100.0, "GOLD"));
}
@Test
void testSilverDiscount() {
assertEquals(90.0, service.calculatePrice(100.0, "SILVER"));
}
}📋 Practical Task
Fixing the Broken LoyaltyPointCalculator
You've inherited a LoyaltyPointCalculator class from a developer who left the company. The business logic is supposed to be:
- Users earn 1 point for every $10 spent.
- "Premium" members earn 1.5 points for every $10 spent.
- Spending less than $0 should throw an
IllegalArgumentException.
However, the current implementation is buggy, and the provided tests are failing. Your task is to fix the calculatePoints method so that all the JUnit 5 tests pass.
// --- The Buggy Implementation ---
public class LoyaltyPointCalculator {
public double calculatePoints(double amount, String memberType) {
if (amount < 0) {
// BUG: This should throw an IllegalArgumentException
return 0;
}
if ("PREMIUM".equalsIgnoreCase(memberType)) {
// BUG: Incorrect multiplier logic
return amount * 0.1;
}
return amount * 0.1; // Standard members
}
}
// --- The Test Suite (Do not change this) ---
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.*;
class LoyaltyPointCalculatorTest {
private LoyaltyPointCalculator calculator;
@BeforeEach
void setUp() {
calculator = new LoyaltyPointCalculator();
}
@Test
void shouldCalculateStandardPoints() {
assertEquals(10.0, calculator.calculatePoints(100.0, "STANDARD"));
}
@Test
void shouldCalculatePremiumPoints() {
assertEquals(15.0, calculator.calculatePoints(100.0, "PREMIUM"));
}
@Test
void shouldThrowExceptionForNegativeAmount() {
assertThrows(IllegalArgumentException.class, () -> {
calculator.calculatePoints(-50.0, "STANDARD");
});
}
}There are no comments for now.