🧪 JUnit Testing with Mockito: Complete Guide for Java Developers

🔰 Introduction to JUnit and Mockito

Testing is a critical aspect of software development that ensures your code works as expected. In the Java ecosystem, JUnit and Mockito are the dynamic duo that make unit testing effective and manageable.

JUnit is a simple yet powerful framework for writing and running repeatable tests in Java. It provides annotations to identify test methods, assertions to verify expected results, and test runners to execute tests.

Mockito complements JUnit by providing a way to create mock objects—simulated objects that mimic the behavior of real objects in controlled ways. This is particularly useful when testing code that depends on external systems or complex objects.

Together, they allow you to:

  • Verify your code works correctly
  • Isolate the code being tested
  • Test components that have external dependencies
  • Develop with confidence using Test-Driven Development (TDD)

🧠 Detailed Explanation

📦 Setting Up JUnit and Mockito

Before diving into testing, you need to set up your project with the necessary dependencies. If you're using Maven, add these to your pom.xml:

<dependencies>
    <!-- JUnit Jupiter API for writing tests -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
    
    <!-- JUnit Jupiter Engine for running tests -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.9.2</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Mockito for mocking -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>5.3.1</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Mockito JUnit Jupiter integration -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.3.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

For Gradle users, add this to your build.gradle:

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
    testImplementation 'org.mockito:mockito-core:5.3.1'
    testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1'
}

test {
    useJUnitPlatform()
}

🧩 JUnit Basics

JUnit 5 (also known as JUnit Jupiter) uses annotations to identify methods that specify a test:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorTest {
    
    @Test
    void addition() {
        Calculator calculator = new Calculator();
        assertEquals(5, calculator.add(2, 3), "2 + 3 should equal 5");
    }
}

Key JUnit annotations include:

  • @Test: Identifies a method as a test method
  • @BeforeEach: Executed before each test method
  • @AfterEach: Executed after each test method
  • @BeforeAll: Executed once before all test methods (must be static)
  • @AfterAll: Executed once after all test methods (must be static)
  • @Disabled: Marks a test as disabled

JUnit also provides various assertion methods:

  • assertEquals(expected, actual): Checks if two values are equal
  • assertTrue(condition): Checks if a condition is true
  • assertFalse(condition): Checks if a condition is false
  • assertNull(object): Checks if an object is null
  • assertNotNull(object): Checks if an object is not null
  • assertThrows(exceptionClass, executable): Checks if code throws an expected exception

🎭 Mockito Fundamentals

Mockito allows you to create and configure mock objects. Here's a basic example:

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class UserServiceTest {
    
    @Test
    void testGetUserName() {
        // Create a mock of UserRepository
        UserRepository mockRepository = mock(UserRepository.class);
        
        // Define behavior for the mock
        when(mockRepository.findById(1L)).thenReturn(new User(1L, "John Doe"));
        
        // Inject the mock into the service
        UserService userService = new UserService(mockRepository);
        
        // Test the service method
        String userName = userService.getUserName(1L);
        
        // Verify the result
        assertEquals("John Doe", userName);
        
        // Verify the mock was called with the expected argument
        verify(mockRepository).findById(1L);
    }
}

Key Mockito methods:

  • mock(Class): Creates a mock object of the given class
  • when(methodCall).thenReturn(value): Defines behavior for a method call
  • when(methodCall).thenThrow(exception): Makes a method throw an exception
  • verify(mock).method(): Verifies a method was called on the mock
  • verify(mock, times(n)).method(): Verifies a method was called n times
  • verify(mock, never()).method(): Verifies a method was never called

🔄 Test Doubles with Mockito

Mockito provides different types of test doubles:

  1. Mocks: Objects that record method calls for verification
List<String> mockedList = mock(List.class);
mockedList.add("one");
verify(mockedList).add("one");
  1. Spies: Partial mocks that use real object behavior unless stubbed
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);

// Real method is called
spyList.add("one");
spyList.add("two");

// Stubbed method
when(spyList.size()).thenReturn(100);

assertEquals(100, spyList.size());
assertEquals("one", spyList.get(0));
  1. Stubs: Objects that return predefined responses
// Create a stub
WeatherService stubService = mock(WeatherService.class);
when(stubService.getCurrentTemperature("London")).thenReturn(25.0);

// Use the stub
WeatherApp app = new WeatherApp(stubService);
double temp = app.getTemperatureInCity("London");
assertEquals(25.0, temp);

🧵 Advanced Mockito Features

Argument Matchers

Mockito provides argument matchers for flexible stubbing and verification:

// Using argument matchers
when(mockRepository.findByName(anyString())).thenReturn(new User(1L, "John"));

// Combining exact values and matchers
when(mockCalculator.add(eq(1), anyInt())).thenReturn(100);

Common matchers include:

  • any(): Matches any object
  • anyInt(), anyString(), etc.: Type-specific matchers
  • eq(value): Matches a specific value
  • contains(substring): Matches strings containing a substring

Capturing Arguments

Capture arguments to perform assertions on them:

@Test
void testEmailSending() {
    EmailService mockEmailService = mock(EmailService.class);
    NotificationService service = new NotificationService(mockEmailService);
    
    service.notifyUser("user@example.com", "Hello!");
    
    ArgumentCaptor<Email> emailCaptor = ArgumentCaptor.forClass(Email.class);
    verify(mockEmailService).sendEmail(emailCaptor.capture());
    
    Email capturedEmail = emailCaptor.getValue();
    assertEquals("user@example.com", capturedEmail.getRecipient());
    assertEquals("Hello!", capturedEmail.getSubject());
}

Stubbing Void Methods

For void methods, use doNothing(), doThrow(), or doAnswer():

// Make void method throw exception
doThrow(new RuntimeException("Database unavailable"))
    .when(mockRepository).save(any(User.class));

// Do nothing when method is called
doNothing().when(mockLogger).logInfo(anyString());

// Custom behavior with Answer
doAnswer(invocation -> {
    String arg = invocation.getArgument(0);
    System.out.println("Called with: " + arg);
    return null;
}).when(mockService).process(anyString());

🚀 Why It Matters / Real-World Use Cases

1. Isolating Components for Testing

In real-world applications, components often depend on databases, web services, or other external systems. Mockito allows you to test components in isolation by mocking these dependencies.

For example, testing a service that processes orders:

@Test
void testOrderProcessing() {
    // Mock dependencies
    PaymentGateway mockPaymentGateway = mock(PaymentGateway.class);
    InventoryService mockInventory = mock(InventoryService.class);
    EmailService mockEmail = mock(EmailService.class);
    
    // Configure mocks
    when(mockInventory.isInStock("PROD-123")).thenReturn(true);
    when(mockPaymentGateway.processPayment(anyDouble())).thenReturn(true);
    
    // Create service with mocks
    OrderService orderService = new OrderService(
        mockPaymentGateway, mockInventory, mockEmail);
    
    // Test order processing
    Order order = new Order("PROD-123", 2, 29.99);
    boolean result = orderService.processOrder(order);
    
    // Verify result and interactions
    assertTrue(result);
    verify(mockInventory).isInStock("PROD-123");
    verify(mockPaymentGateway).processPayment(59.98); // 2 * 29.99
    verify(mockInventory).decreaseStock("PROD-123", 2);
    verify(mockEmail).sendOrderConfirmation(any(Order.class));
}

2. Test-Driven Development (TDD)

JUnit and Mockito are essential tools for TDD, where you write tests before implementing functionality. This approach leads to better design and more maintainable code.

TDD workflow with JUnit and Mockito:

  1. Write a failing test that defines the expected behavior
  2. Implement the minimum code to make the test pass
  3. Refactor the code while keeping tests passing

3. Regression Testing

As your codebase evolves, JUnit tests serve as regression tests to ensure new changes don't break existing functionality. Mockito helps keep these tests focused and fast by eliminating external dependencies.

4. Testing Edge Cases and Error Handling

Mockito makes it easy to test how your code handles exceptional conditions:

@Test
void testDatabaseConnectionFailure() {
    // Mock repository to throw exception
    UserRepository mockRepo = mock(UserRepository.class);
    when(mockRepo.findById(anyLong()))
        .thenThrow(new DatabaseConnectionException("Connection refused"));
    
    UserService service = new UserService(mockRepo);
    
    // Verify service handles the exception gracefully
    assertThrows(ServiceUnavailableException.class, () -> {
        service.getUserDetails(123L);
    });
}

🧭 Best Practices / Rules to Follow

✅ Do's

  1. Keep tests simple and focused

    • Each test should verify one specific behavior
    • Use descriptive test names that explain what's being tested
  2. Follow the AAA pattern

    • Arrange: Set up test objects and mocks
    • Act: Call the method being tested
    • Assert: Verify the expected outcome
  3. Use appropriate assertions

    • Choose the most specific assertion for your case
    • Include meaningful error messages in assertions
  4. Mock only what's necessary

    • Only mock external dependencies or complex objects
    • Use real objects for simple dependencies
  5. Organize tests logically

    • Group related tests in the same class
    • Use nested classes for different test scenarios
  6. Reset mocks between tests

    • Use @BeforeEach to reset or recreate mocks
    • Or use @ExtendWith(MockitoExtension.class) with @Mock annotations
  7. Test both happy paths and edge cases

    • Verify normal operation works correctly
    • Test boundary conditions and error handling

❌ Don'ts

  1. Don't mock everything

    • Excessive mocking leads to brittle tests
    • Tests should verify behavior, not implementation details
  2. Avoid complex test setup

    • If setup is complex, your design might need improvement
    • Extract common setup into helper methods
  3. Don't test trivial code

    • Skip testing simple getters/setters
    • Focus on code with logic or business rules
  4. Don't use mocks for value objects

    • Create real instances of simple objects
    • Mock only objects with behavior
  5. Don't overspecify interactions

    • Verify only the interactions that matter
    • Too many verifications make tests brittle

⚠️ Common Pitfalls or Gotchas

1. Overusing Mocks

Problem: Mocking every dependency leads to tests that are tightly coupled to implementation details.

// Too much mocking
@Test
void overMockedTest() {
    User mockUser = mock(User.class);
    Address mockAddress = mock(Address.class);
    when(mockUser.getAddress()).thenReturn(mockAddress);
    when(mockAddress.getZipCode()).thenReturn("12345");
    
    // Test becomes brittle and complex
}

// Better approach
@Test
void betterTest() {
    User user = new User("John", new Address("Street", "City", "12345"));
    // Test with real objects for simple cases
}

2. Mocking Static Methods

Problem: Static methods are harder to mock and often indicate design issues.

Solution: Refactor to use instance methods or dependency injection, or use Mockito's static mocking capabilities (requires mockito-inline):

// Using mockito-inline for static methods
@Test
void testStaticMethod() {
    try (MockedStatic<Utility> utilities = mockStatic(Utility.class)) {
        utilities.when(() -> Utility.getCurrentDate()).thenReturn(LocalDate.of(2023, 1, 1));
        
        // Test code that uses Utility.getCurrentDate()
        assertEquals("2023-01-01", DateProcessor.formatCurrentDate());
    }
}

3. Stubbing Methods You Don't Call

Problem: Stubbing methods that aren't called in the test leads to unclear tests and potential issues.

Solution: Only stub methods that will be called during the test:

@Test
void testUserService() {
    UserRepository mockRepo = mock(UserRepository.class);
    
    // Only stub methods that will be called
    when(mockRepo.findById(1L)).thenReturn(new User(1L, "John"));
    
    UserService service = new UserService(mockRepo);
    User user = service.getUser(1L);
    
    assertEquals("John", user.getName());
}

4. Incorrect Argument Matchers

Problem: Mixing concrete values and argument matchers in the same method call.

Solution: Use eq() for concrete values when using other matchers:

// Incorrect - will throw InvalidUseOfMatchersException
when(mockService.process("specific-value", anyString())).thenReturn(true);

// Correct
when(mockService.process(eq("specific-value"), anyString())).thenReturn(true);

5. Not Resetting Mocks Between Tests

Problem: State from one test affecting another test.

Solution: Reset mocks or create new ones for each test:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository mockRepository;
    
    @InjectMocks
    private UserService userService;
    
    // Each test gets fresh mocks
    @Test
    void testGetUser() { /* ... */ }
    
    @Test
    void testCreateUser() { /* ... */ }
}

6. Mocking Exceptions Incorrectly

Problem: Incorrect exception mocking syntax.

Solution: Use the correct syntax for the method type:

// For methods with return values
when(mockRepo.findById(anyLong())).thenThrow(new NotFoundException());

// For void methods
doThrow(new DatabaseException()).when(mockRepo).delete(any(User.class));

📌 Summary / Key Takeaways

  • JUnit provides the framework for writing and running tests in Java
  • Mockito complements JUnit by allowing you to create mock objects for dependencies
  • Together, they enable effective unit testing by isolating components
  • Key JUnit features include annotations (@Test, @BeforeEach, etc.) and assertions
  • Key Mockito features include mocking, stubbing, and verification
  • Follow best practices like keeping tests focused, using the AAA pattern, and mocking only what's necessary
  • Avoid common pitfalls like overusing mocks, incorrect argument matchers, and not resetting mocks
  • Real-world applications include isolating components, TDD, regression testing, and testing edge cases

🧩 Exercises or Mini-Projects

Exercise 1: User Authentication System

Create a simple user authentication system with the following components:

  1. User class with username, password, and role fields
  2. UserRepository interface with methods to find and save users
  3. AuthenticationService class that depends on UserRepository and provides login functionality

Write JUnit tests for AuthenticationService using Mockito to mock the UserRepository. Your tests should cover:

  • Successful login
  • Failed login due to incorrect password
  • Failed login due to user not found
  • Password validation rules

Exercise 2: Shopping Cart with Discount Service

Implement a shopping cart system with these components:

  1. Product class with id, name, and price
  2. DiscountService interface that calculates discounts based on products and quantities
  3. ShoppingCart class that uses DiscountService to calculate the final price

Write tests for ShoppingCart using Mockito to mock the DiscountService. Your tests should verify:

  • Correct calculation of total price without discounts
  • Correct application of percentage discounts
  • Correct application of fixed amount discounts
  • Edge cases like empty cart and maximum discount limits