Lesson 37 • Advanced
Unit Testing with JUnit & Mockito
Tests aren't just "nice to have" — they're your safety net. Every time you refactor, add features, or fix bugs, tests tell you instantly if something broke. JUnit 5 + Mockito is the gold standard for Java testing, and mastering this combo makes you a reliable developer that teams trust.
Before You Start
You should know OOP (classes, interfaces for mocking) and Annotations (JUnit is annotation-driven). Familiarity with Exception Handling helps for assertThrows testing.
What You'll Learn
- ✅ JUnit 5 lifecycle: @Test, @BeforeEach, @AfterEach
- ✅ Assertions: assertEquals, assertThrows, assertAll
- ✅ Parameterized tests with @ParameterizedTest
- ✅ Mocking dependencies with Mockito
- ✅ Test-Driven Development (TDD) workflow
- ✅ Code coverage and testing best practices
1️⃣ The Testing Pyramid
Analogy: Unit tests are like spell-checking individual words — fast, cheap, catch most errors. Integration tests check that sentences make sense together. E2E tests verify the whole essay reads correctly. You want a LOT of unit tests, some integration tests, and few E2E tests.
| Level | Speed | Scope | Target |
|---|---|---|---|
| Unit | ⚡ ms | Single method/class | 70% of tests |
| Integration | 🔄 seconds | Multiple components + DB | 20% of tests |
| E2E | 🐢 minutes | Full application | 10% of tests |
Try It: JUnit Test Suite
// 💡 Try modifying this code and see what happens!
// Build a complete test suite with assertions
console.log("=== JUnit Test Suite ===\n");
// Mini test framework
let passed = 0, failed = 0;
function assertEquals(expected, actual, msg) {
if (expected === actual) { passed++; console.log(" ✅ " + msg); }
else { failed++; console.log(" ❌ " + msg + " (expected " + expected + ", got " + actual + ")"); }
}
function assertTrue(condition, msg) {
if (condition) { passed++; console.log(" ✅ " +
...2️⃣ Arrange → Act → Assert
Every well-structured test follows this three-step pattern. It keeps your tests readable and focused:
1. Arrange: Set up test data and dependencies
2. Act: Call the method you're testing
3. Assert: Verify the result is what you expected
Try It: Mockito — Isolating Dependencies
// 💡 Try modifying this code and see what happens!
// Simulating Mockito — mocking external dependencies
console.log("=== Mockito — Mocking Dependencies ===\n");
// The problem: UserService depends on UserRepository (database)
// We can't test UserService without a real database... or CAN we?
// Mock UserRepository (simulating @Mock)
class MockUserRepository {
constructor() { this.calls = []; this.returnValues = {}; }
// when(mock.method(args)).thenReturn(value)
whenFindById(id, ret
...Common Mistakes
- ⚠️ Testing implementation, not behavior: Don't assert that a specific private method was called. Assert the outcome
- ⚠️ Testing too much per test: One logical concept per test. If it fails, you should immediately know why
- ⚠️ Not testing edge cases: Test null inputs, empty lists, boundary values, error conditions
- ⚠️ Mocking everything: Only mock external dependencies (DB, API). Test real logic with real objects
- ⚠️ Ignoring flaky tests: A test that sometimes passes is worse than no test — it erodes trust
Pro Tips
- 💡 @DisplayName — give tests human-readable names:
@DisplayName("should throw when email is blank") - 💡 @ParameterizedTest — run the same test with 20 different inputs in one declaration
- 💡 assertAll() — check multiple assertions at once. All failures are reported, not just the first
- 💡 TDD workflow: 🔴 Write failing test → 🟢 Write minimal code to pass → 🔵 Refactor while tests stay green
Try It: Parameterized Tests & TDD
// 💡 Try modifying this code and see what happens!
// Parameterized tests and TDD workflow
console.log("=== Parameterized Tests & TDD ===\n");
// TDD Example: Build a password validator step by step
// 🔴 RED: Write failing tests first
console.log("1. TDD STEP 1 — 🔴 RED (write tests first):");
console.log(" Tests define the requirements BEFORE writing code\n");
// 🟢 GREEN: Implement to make tests pass
class PasswordValidator {
validate(password) {
let errors = [];
if (!password
...📋 Quick Reference
| Annotation | Purpose | Example |
|---|---|---|
| @Test | Mark test method | Required on each test |
| @BeforeEach | Setup before each test | Initialize fresh objects |
| @ParameterizedTest | Run with multiple inputs | @CsvSource, @ValueSource |
| @Mock | Create mock object | Mockito annotation |
| verify() | Check mock was called | verify(mock).method() |
🎉 Lesson Complete!
You can now write professional unit tests that catch bugs before users do! Next: Maven & Gradle — build tools and dependency management.
Sign up for free to track which lessons you've completed and get learning reminders.