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
124: Assertions and Test Lifecycle Annotations
I've spent a lot of time in the trenches of legacy codebases, and one thing I've noticed is that developers often confuse assert statements with validation logic. They aren't the same thing. Let's look at why, and then we'll tackle how to actually organize our tests so we aren't writing the same setup code twenty times a file.
The vanishing act of the assert keyword
Suppose I'm building a simple BankAccount class. I have a rule: the balance should never drop below zero. I'll use a Java assertion to enforce this invariant during development.
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void withdraw(double amount) {
balance -= amount;
// I want to make sure the balance didn't go negative
assert balance >= 0 : "Balance cannot be negative! Current balance: " + balance;
}
public double getBalance() {
return balance;
}
}
Now, if I run a main method and call withdraw(200) on an account with only 100, I expect the program to scream at me with an AssertionError. I run it... and nothing happens. The program just continues, and the balance becomes -100.0. I'm thinking, "Wait, did I misspell the keyword?"
Nope. Here is the quirk: assertions are disabled by default in the JVM. Java assumes that in a production environment, you don't want the performance hit of checking these invariants. To actually see that error, I have to pass the -ea (enable assertions) flag to the JVM at runtime.
Once I add -ea to my VM arguments, the program crashes exactly where I expected. This is a key distinction: you use assert for things that "should be impossible" if your code is correct. You use if (amount > balance) throw new IllegalArgumentException(...) for things that can actually happen due to bad user input. Assertions are for the developer; exceptions are for the runtime.
Cleaning up the repetitive setup
Now let's move into the testing phase. I'm writing a JUnit 5 test suite for this account. I start writing my tests, and it looks like this:
class BankAccountTest {
@Test
void testWithdraw() {
BankAccount account = new BankAccount(100);
account.withdraw(50);
assertEquals(50, account.getBalance());
}
@Test
void testDeposit() {
BankAccount account = new BankAccount(100);
account.deposit(50);
assertEquals(150, account.getBalance());
}
}
I'm already annoyed. I'm instantiating new BankAccount(100) in every single method. If I decide later that accounts should start with 0, I have to change it in ten different places. I need a way to "reset the stage" before every test.
This is where @BeforeEach comes in. I'll move the account creation to a separate method. Now, JUnit will execute this method automatically before every single @Test method in the class.
class BankAccountTest {
private BankAccount account;
@BeforeEach
void setUp() {
account = new BankAccount(100);
System.out.println("Setting up a fresh account...");
}
@Test
void testWithdraw() {
account.withdraw(50);
assertEquals(50, account.getBalance());
}
@Test
void testDeposit() {
account.deposit(50);
assertEquals(150, account.getBalance());
}
}
The code is cleaner, and the tests are isolated. If testWithdraw messes up the account state, testDeposit doesn't care because it gets a brand new instance.
Handling the heavy lifting
But what if I have something expensive? Imagine I'm connecting to a real database to verify my account logs. Creating a database connection takes two seconds. If I have 100 tests, @BeforeEach would add over three minutes to my build time. That's unacceptable.
I need something that runs once for the entire class, not once per method. That's @BeforeAll. There's a catch, though: because it runs before the class is even instantiated for the individual tests, the method must be static.
class BankAccountTest {
private static DatabaseConnection db;
private BankAccount account;
@BeforeAll
static void initSharedResources() {
db = new DatabaseConnection("jdbc:mysql://localhost:3306/bank");
db.connect();
System.out.println("Database connected once for all tests.");
}
@BeforeEach
void setUp() {
account = new BankAccount(100);
}
@AfterAll
static void tearDownSharedResources() {
db.disconnect();
System.out.println("Database closed.");
}
}
I've now got a hierarchy of lifecycles. @BeforeAll handles the heavy, shared infrastructure; @BeforeEach handles the lightweight, fresh state for every test. If I had files to delete or sockets to close after a specific test, I'd use @AfterEach. It's all about balancing performance with test isolation.
📋 Practical Task
Implementing the InventoryManager Lifecycle
You are tasked with testing an InventoryManager class. This class tracks product stock and connects to an external InventoryLogFile (simulated) to record changes. Your goal is to set up a test suite that is efficient and clean.
Requirements:
- The Class: Create an
InventoryManagerclass with astock(int) and arecordChange(String msg)method. - The Assertion: Inside
InventoryManager.removeStock(int amount), add a Javaassertstatement to ensure the stock never drops below zero. - The Lifecycle: Create a JUnit 5 test class
InventoryManagerTest:- Use
@BeforeAllto simulate opening a log file (just print "Log File Opened"). This method must be static. - Use
@BeforeEachto initialize theInventoryManagerwith 10 items before every test. - Use
@AfterAllto simulate closing the log file (just print "Log File Closed").
- Use
- The Tests: Write one test that verifies removing stock works, and another that would trigger your
assertstatement (remember to mention how you would run it with-eain a comment).
There are no comments for now.