Appearance
Unit Testing Roadmap | JUnit with Mockito: Basics to Advanced
The First Line of Defense
Imagine a large hospital that wants to ensure every new piece of medical equipment works before it is used on a patient. The engineers do not simply plug the machine into the full hospital network and hope for the best. They test each component in a controlled environment — a single sensor, a single circuit board — before it ever interacts with the rest of the system. Only after each individual part is proven correct do the engineers wire everything together.
Software engineering works the same way. Before code reaches production, before integration tests run, before a QA analyst opens a browser, there is a layer of testing that verifies each individual method or function works exactly as written. That layer is unit testing, and it is considered the first line of defense against bugs.
This lecture is your roadmap for everything covered in this course — from writing your first JUnit 5 test case, to mocking complex dependencies with Mockito, to generating coverage reports and integrating with CI pipelines.
What Is Unit Testing?
Unit testing is the process of testing an individual unit of source code — typically a single method or function — in isolation, verifying that it behaves as expected.
The key phrase is "in isolation." You are not testing how your service layer communicates with your database. You are not testing an end to end user flow. You are testing one method, with known inputs, and asserting a known output.
java
// The unit under test
public class Calculator {
public int multiply(int a, int b) {
return a * b;
}
}
// A simple unit test
class CalculatorTest {
@Test
void multiply_shouldReturnProduct_whenGivenTwoIntegers() {
// Arrange
Calculator calculator = new Calculator();
// Act
int result = calculator.multiply(4, 2);
// Assert
assertEquals(8, result);
}
}This is a unit test. You pass known values (4 and 2), and you assert the expected result (8). If someone later accidentally changes return a * b to return a * b - 1, this test immediately catches the regression.
Why Unit Testing Matters
Early Bug Detection
Catching a bug while writing the method is exponentially cheaper than catching it in production. A unit test pinpoints the exact method where the behavior deviated from expectation.
Refactor Confidently
When you refactor a method — say, replacing a for loop with a Java Stream — the input and output contract should remain identical. Your existing unit tests act as a safety net. If they pass after the refactor, you have confidence the behavior is preserved.
Living Documentation
A well written test suite is the most accurate documentation a codebase can have. Unlike comments that go stale, tests fail when they no longer reflect reality. Reading a test immediately tells you: "if I give this method these inputs, I get this output."
Save Cost
Fixing a bug during development takes minutes. Fixing the same bug in production may take hours of debugging, hotfixes, rollbacks, customer support, and post mortems.
The Testing Ecosystem
JUnit — The Testing Framework
JUnit is the most widely adopted Java testing framework. This course covers JUnit 5, which is the current recommended version for all new development. JUnit provides:
- Annotations like
@Test,@BeforeEach,@AfterAllto structure test lifecycle - The ability to run tests via Maven, Gradle, IntelliJ, Eclipse, or the command line
- An extensible architecture (covered in depth in the architecture lecture)
Other frameworks like TestNG exist, but JUnit 5 is the industry standard.
Mockito — The Mocking Framework
JUnit alone cannot mock dependencies. Consider this scenario:
ClassA.methodA()
└── calls ClassB.methodB()
└── calls ClassC.methodC()
└── calls external databaseWhen you unit test ClassA.methodA(), you want to test only that method in isolation. You do not want ClassB, ClassC, or the database involved. Mocking replaces those external dependencies with controlled substitutes.
Popular mocking frameworks:
- Mockito — the most widely used, the focus of this course
- EasyMock — another well known option
- JMock — less common in modern projects
Mockito allows you to:
java
// Mock a dependency
UserRepository mockRepository = mock(UserRepository.class);
// Control what the mock returns
when(mockRepository.findById(1L)).thenReturn(Optional.of(new User("Alice")));
// Verify the dependency was called
verify(mockRepository, times(1)).findById(1L);You can mock static methods, private methods (via PowerMock or Mockito's inline mocking), final classes, and void methods. All of these scenarios are covered in this course.
AssertJ — The Fluent Assertion Library
JUnit ships with basic assertions like assertEquals, assertTrue, and assertFalse. However, AssertJ provides a fluent, chainable API that makes assertions far more readable and expressive.
java
// JUnit built-in style
assertEquals("Alice", user.getName());
assertTrue(user.isActive());
assertNotNull(user.getEmail());
// AssertJ fluent style
assertThat(user)
.extracting(User::getName, User::isActive, User::getEmail)
.containsExactly("Alice", true, "alice@example.com");AssertJ supports deep collection assertions, string assertions, exception assertions, and much more. This course integrates AssertJ alongside JUnit 5 and Mockito.
Full Course Roadmap
Here is the complete list of topics this course covers, from foundational concepts to advanced production techniques.
JUnit 5 — Foundations
| Topic | What You Will Learn |
|---|---|
| Architecture | JUnit Platform, Jupiter, Vintage — how the three modules work together |
| Assertions | assertEquals, assertThrows, assertAll, assertTimeout and 15+ more |
| Assumptions | assumeTrue, assumeFalse, assumingThat — skip tests conditionally |
| Test Lifecycle | @BeforeAll, @BeforeEach, @AfterEach, @AfterAll, per method vs per class |
| Parallel Execution | Running test methods and classes concurrently with junit-platform.properties |
| AAA Pattern | Arrange, Act, Assert — the structural foundation of every test |
@RepeatedTest | Run a test N times with custom display names and repetition info |
@ParameterizedTest | Supply multiple input sets to a single test method |
@TestMethodOrder | Control the order in which test methods execute |
| Conditional Execution | @EnabledOnOs, @EnabledIfEnvironmentVariable, etc. |
| Nested Tests | Organize related test cases using @Nested inner classes |
| JUnit 5 Extensions | @ExtendWith, ParameterResolver, custom extensions |
| Tagging and Filtering | @Tag to group and selectively run tests |
| Test Suites | @Suite, @SelectPackages, @SelectClasses |
Mockito — Mocking in Depth
| Topic | What You Will Learn |
|---|---|
| Mockito Basics | mock(), when(), thenReturn(), verify() |
@Mock and @InjectMocks | Annotation driven mocking with JUnit 5 extension |
| Argument Matchers | any(), eq(), argThat(), captor |
| Stubbing Void Methods | doNothing(), doThrow(), doAnswer() |
| Spy | Partial mocking with @Spy and spy() |
| Static Method Mocking | MockedStatic — mocking static methods |
| Final Class Mocking | Inline mocking for final classes |
| Verification | verify(), verifyNoMoreInteractions(), inOrder() |
| Capturing Arguments | ArgumentCaptor for verifying complex arguments |
| Mockito with Spring Boot | @MockBean, @SpyBean, @WebMvcTest, @DataJpaTest |
AssertJ — Advanced Assertions
| Topic | What You Will Learn |
|---|---|
| Core Assertions | Fluent chaining for objects, strings, numbers, booleans |
| Collection Assertions | containsExactly, containsAnyOf, extracting, filteredOn |
| Exception Assertions | assertThatThrownBy, assertThatExceptionOfType |
| Soft Assertions | Collect all failures before reporting |
| Custom Assertions | Write your own domain specific assertion classes |
Advanced and Production Topics
| Topic | What You Will Learn |
|---|---|
| Test Driven Development | Write tests before writing implementation (Red, Green, Refactor) |
| Code Coverage with JaCoCo | Measure and enforce coverage thresholds |
| GitHub Actions Integration | Run your test suite on every commit in CI |
| AI Assisted Test Writing | Emerging tools for generating test cases with AI |
Setting Up Your Project
To follow along, you need a Java project with these core dependencies in your pom.xml:
xml
<dependencies>
<!-- JUnit 5 + Mockito + AssertJ via Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>spring-boot-starter-test transitively includes:
junit-jupiter(JUnit 5 test engine)mockito-coreassertj-corehamcrestspring-test
If you are not using Spring Boot, add JUnit 5 and Mockito directly:
xml
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>Running all tests from the command line:
bash
mvn testWhat "In Isolation" Really Means
A common source of confusion: when should you mock a dependency?
Mock it when the dependency lives in a different class, especially if that class:
- Makes network calls
- Reads from or writes to a database
- Has its own complex dependencies
- Is slow or nondeterministic
Do not mock it when the code lives in the same class as the method under test — even if it is a separate private method. Dividing a large method into small private helpers is an organizational choice. Those helpers are part of the same logical unit. Mocking them would mean you are not actually testing your own logic.
java
public class OrderService {
private final PaymentGateway paymentGateway; // Mock this — different class
public OrderResult placeOrder(Order order) {
validate(order); // Same class private method — do NOT mock
double total = calculateTotal(order); // Same class — do NOT mock
return paymentGateway.charge(total); // Different class — MOCK this
}
private void validate(Order order) { /* ... */ }
private double calculateTotal(Order order) { /* ... */ }
}Interview Questions & Pitfalls
Q1: What is unit testing and how does it differ from integration testing?
Unit testing verifies a single method or function in isolation, mocking all external dependencies. Integration testing wires multiple components together — for example, a service layer with a repository and a real database — and verifies the combined behavior. Unit tests are fast and pinpoint failures precisely; integration tests are slower and verify real wiring.
Q2: Why is Mockito needed alongside JUnit 5?
JUnit 5 provides the framework to declare and run tests, manage lifecycle, and make assertions. It has no ability to create fake implementations of dependencies. Mockito fills that gap by providing mock objects that you can program to return specific values and verify were called with specific arguments.
Q3: What is the difference between mocking a method in a different class versus a private method in the same class?
A method in a different class is an external dependency that should be mocked to preserve isolation. A private method in the same class is part of the same logical unit; mocking it would prevent you from testing your own class's internal logic and would couple your test to implementation details rather than behavior.
Q4: What is AssertJ and why prefer it over JUnit's built in assertions?
AssertJ provides a fluent, chainable API that reads like natural English: assertThat(result).isNotNull().isEqualTo(expected). JUnit's built in assertions like assertEquals(expected, actual) require remembering argument order and produce less descriptive failure messages. AssertJ also offers rich collection and exception assertions out of the box.
Q5: What does "first line of defense" mean in the context of unit testing?
Unit tests are the earliest check in the software delivery pipeline. Before the code is compiled into a deployable artifact, before it is deployed to any environment, and before any human tests it manually, the unit tests run. They catch regressions immediately at the point of code change, making them the cheapest and fastest mechanism to detect and prevent bugs.
Q6: When should you NOT use unit testing alone?
Unit tests cannot verify that Spring's dependency injection is wired correctly, that your SQL queries return the right data from a real database, or that an HTTP response body is serialized correctly end to end. Those scenarios require integration or functional tests. A healthy project has a layered strategy: many unit tests for logic, fewer integration tests for wiring, and even fewer end to end functional tests for critical user flows.