Skip to content

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, @AfterAll to 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 database

When 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

TopicWhat You Will Learn
ArchitectureJUnit Platform, Jupiter, Vintage — how the three modules work together
AssertionsassertEquals, assertThrows, assertAll, assertTimeout and 15+ more
AssumptionsassumeTrue, assumeFalse, assumingThat — skip tests conditionally
Test Lifecycle@BeforeAll, @BeforeEach, @AfterEach, @AfterAll, per method vs per class
Parallel ExecutionRunning test methods and classes concurrently with junit-platform.properties
AAA PatternArrange, Act, Assert — the structural foundation of every test
@RepeatedTestRun a test N times with custom display names and repetition info
@ParameterizedTestSupply multiple input sets to a single test method
@TestMethodOrderControl the order in which test methods execute
Conditional Execution@EnabledOnOs, @EnabledIfEnvironmentVariable, etc.
Nested TestsOrganize 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

TopicWhat You Will Learn
Mockito Basicsmock(), when(), thenReturn(), verify()
@Mock and @InjectMocksAnnotation driven mocking with JUnit 5 extension
Argument Matchersany(), eq(), argThat(), captor
Stubbing Void MethodsdoNothing(), doThrow(), doAnswer()
SpyPartial mocking with @Spy and spy()
Static Method MockingMockedStatic — mocking static methods
Final Class MockingInline mocking for final classes
Verificationverify(), verifyNoMoreInteractions(), inOrder()
Capturing ArgumentsArgumentCaptor for verifying complex arguments
Mockito with Spring Boot@MockBean, @SpyBean, @WebMvcTest, @DataJpaTest

AssertJ — Advanced Assertions

TopicWhat You Will Learn
Core AssertionsFluent chaining for objects, strings, numbers, booleans
Collection AssertionscontainsExactly, containsAnyOf, extracting, filteredOn
Exception AssertionsassertThatThrownBy, assertThatExceptionOfType
Soft AssertionsCollect all failures before reporting
Custom AssertionsWrite your own domain specific assertion classes

Advanced and Production Topics

TopicWhat You Will Learn
Test Driven DevelopmentWrite tests before writing implementation (Red, Green, Refactor)
Code Coverage with JaCoCoMeasure and enforce coverage thresholds
GitHub Actions IntegrationRun your test suite on every commit in CI
AI Assisted Test WritingEmerging 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-core
  • assertj-core
  • hamcrest
  • spring-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 test

What "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.