Testing Java Classes with JUnit and Mockito

Testing Java Classes with JUnit and Mockito

Unit testing is a vital practice in software development that ensures the reliability and functionality of individual components of your application. While JUnit provides a robust framework for writing unit tests, Mockito takes it to the next level by enabling mocking, which simplifies testing classes that depend on other components.

This tutorial will introduce you to mocking in Java using Mockito, explain how to create mocks with @Mock and @InjectMocks, demonstrate stubbing with when().thenReturn(), and describe how to verify interactions with verify(). By the end, you’ll know how to test Java classes effectively using JUnit and Mockito.

Table of Contents

  1. What is Mocking?
  2. Creating Mocks Using @Mock and @InjectMocks
  3. Stubbing Methods with when().thenReturn()
  4. Verifying Interactions with verify()
  5. Best Practices for Mocking
  6. Final Thoughts

What is Mocking?

Mocking is the process of creating fake objects that mimic the behavior of real objects. These mock objects simulate dependencies, making it easier to test a class in isolation.

For example, if you’re testing a class that interacts with a database or an external API, you can use a mock object in place of the actual database or API. This approach ensures that your tests are fast and focused solely on the logic of the class being tested.

Key benefits of mocking include:

  1. Isolation: Test only the class under test, not its dependencies.
  2. Controlled Behavior: Simulate specific scenarios or error conditions.
  3. Performance: Avoid slow operations like database queries or API calls.

Real-World Example

Imagine testing a UserService class that depends on a UserRepository to fetch users. Instead of connecting to a database, you can mock the UserRepository to return predefined data. This simplifies your tests and eliminates dependency on the real database.


Creating Mocks Using @Mock and @InjectMocks

Mockito simplifies mock creation with annotations like @Mock and @InjectMocks. These annotations eliminate boilerplate code and make your tests more readable.

Setting Up Mockito for Your Project

Add the necessary dependencies to your project’s build file.

For Maven:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

For Gradle:

testImplementation 'org.mockito:mockito-core:5.5.0'

For advanced features like annotations (@Mock@InjectMocks), include the mockito-junit-jupiter dependency:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

Understanding @Mock

The @Mock annotation tells Mockito to create a mock object for the specified class or interface.

Example:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    private UserService userService;

    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this); // Initialize mocks
        userService = new UserService(userRepository);
    }

    @Test
    void testFindById() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(new User("John Doe")));

        User user = userService.findById(1L);
        assertNotNull(user);
        assertEquals("John Doe", user.getName());
    }
}

Key Points:

  • @Mock reduces boilerplate code for creating mock objects.
  • Use MockitoAnnotations.openMocks(this) in a @BeforeEach method to initialize mocks.

Using @InjectMocks

The @InjectMocks annotation tells Mockito to inject mock dependencies into an object automatically.

Example with @InjectMocks:

class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService; // Automatically injects userRepository mock

    @Test
    void testFindByUsername() {
        when(userRepository.findByUsername("jdoe")).thenReturn(new User("John Doe"));

        User user = userService.findByUsername("jdoe");
        assertEquals("John Doe", user.getName());
    }
}

By using @InjectMocks, you no longer need to manually instantiate the class under test, saving time and reducing setup complexity.


Stubbing Methods with when().thenReturn()

Stubbing allows you to define the behavior of a mock method when it’s called. Use when().thenReturn() to specify the input-output behavior of mock methods.

Syntax:

when(mock.method(input)).thenReturn(output);

Example:

@Test
void testUserByEmail() {
    when(userRepository.findByEmail("johndoe@example.com")).thenReturn(new User("John Doe"));

    User user = userService.findByEmail("johndoe@example.com");
    assertNotNull(user);
    assertEquals("John Doe", user.getName());
}

Stubbing Exceptions

Use thenThrow() to simulate an exception:

@Test
void testUserNotFound() {
    when(userRepository.findById(anyLong())).thenThrow(new NoSuchElementException("User not found"));

    assertThrows(NoSuchElementException.class, () -> userService.findById(99L));
}

Advanced Stubbing with Argument Matchers

Mockito’s any()eq(), and other matchers allow dynamic stubbing:

when(userRepository.findById(anyLong())).thenReturn(new User("Default User"));

Verifying Interactions with verify()

Mockito’s verify() method checks whether certain methods were called on a mock and how often they were called.

Syntax:

verify(mock).method(args);

Verifying Interaction

Check that a method was called:

@Test
void testSaveUser() {
    User user = new User("Jane Doe");
    userService.saveUser(user);

    verify(userRepository).save(user); // Verifies save() was called
}

Verifying Call Counts

Specify how often a method should be invoked:

verify(mock, times(1)).method(args);  // Exactly once  
verify(mock, never()).method(args);  // Never called
verify(mock, atLeast(2)).method(args); // At least twice
verify(mock, atMost(3)).method(args); // At most three times

Example:

verify(userRepository, times(1)).save(any(User.class));
verify(userRepository, never()).delete(any(User.class));

Using verify() ensures your tests aren’t just checking the final state but also validating interactions between classes.


Best Practices for Mocking

  1. Mock Only External Dependencies: Avoid mocking classes you control. Use mocks only for interfaces or external services (e.g., DBs, APIs).
  2. Use Clear Mocks: Keep mock interactions simple and predictable to avoid test complexity.
  3. Avoid Over-Mocking: Limit mocking to the dependencies of the class under test and avoid mocking internal logic.
  4. Combine Assertions with Verifications: Verify both behavior (interactions) and state (assertions) to ensure comprehensive testing.
  5. Refactor Common Behavior: Move repetitive setups (e.g., @Mock initialization) to a @BeforeEach method.

Final Thoughts

Mockito and JUnit together provide a powerful toolkit for testing Java classes. By learning how to create mocks with @Mock and @InjectMocks, stub methods with when().thenReturn(), and verify interactions using verify(), you can write tests that are both reliable and maintainable.

Start experimenting with simple examples, then integrate mocking into your more complex projects. Over time, you’ll gain confidence in your application’s quality and deliver better code to production with fewer headaches. Happy testing!

Testing Java Classes with JUnit and Mockito

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top