Comprehensive Guide to JUnit and Mockito Testing

10 minute read

Introduction

This comprehensive guide covers JUnit and Mockito testing frameworks for Java applications. It focuses on unit testing, integration testing, and best practices for writing maintainable and reliable tests.

Key Concepts:

  • Class MyService is SUT (System Under Test)
  • Class SomeService is Dependency (which is mocked)
  • Isolation of components for focused testing
  • Mocking external dependencies

Example Code

Feature Mockito Unit Tests Spring Boot Integration Tests
Purpose Test individual units in isolation. Test the interaction of multiple units in a Spring context.
Testing Framework Mockito (or JUnit + other testing frameworks). Spring Boot Test (JUnit is commonly used).
Annotation for Test Class @RunWith(MockitoJUnitRunner.class) or @ExtendWith(MockitoExtension.class) @SpringBootTest
Mock Creation Annotation @Mock @MockBean
Application Context Not required (Mocks are manually injected). Automatically loads Spring application context.
Dependency Injection Manual (Mocks injected using @InjectMocks) Automatic (Spring Boot injects mocks using or @Autowired or @MockBean).

Autowired vs InjectMock

@Autowired:

  • Use @Autowired when Spring should inject real beans (actual instances managed by the Spring container) into Spring-managed components.
  • It’s a Spring Framework annotation for automatic dependency injection in Spring-managed components (e.g., services, controllers, and repositories).
  • Spring injects the dependency into the field or constructor of the class when @Autowired is used.
  • Commonly employed in integration tests or when testing Spring components that rely on other Spring-managed beans.

@InjectMocks:

  • Use @InjectMocks when Mockito should inject mocks into the fields of your test class for unit testing.
  • It’s a Mockito annotation used to automatically inject mocked dependencies into the fields of a test class.
  • Especially useful when testing a class in isolation and controlling the behavior of its dependencies.
  • Typically applied in unit tests when mocking the dependencies of the class under test.

Mock vs MockBean

@Mock:

  • Part of the Mockito framework and used for creating mock objects in unit tests.
  • Mockito creates a mock for each field annotated with @Mock and injects the mocks into fields annotated with @InjectMocks.

@MockBean:

  • Used in Spring Boot tests, particularly for integration testing.
  • When using @MockBean, you create a mock of a Spring bean.
  • It’s beneficial when replacing a real bean with a mock in the Spring application context during the test.

Unit Testing

  • with @ExtendWith(MockitoExtension.class)
  • Used in conjunction with Mockito annotations like @Mock, @InjectMocks, etc.
  • Does not start the Spring context; it’s focused on unit testing.
@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyClassTest {
    
    @BeforeAll
    static void setUpBeforeClass() {
        // Runs once before all test methods in this class
        // Use for expensive setup operations
    }
    
    @BeforeEach
    void setUp() {
        // Runs before each test method
        MockitoAnnotations.openMocks(this);
    }
    
    @AfterEach
    void tearDown() {
        // Runs after each test method
        // Clean up resources
    }
    
    @AfterAll
    static void tearDownAfterClass() {
        // Runs once after all test methods
    }
}

Integration Testing

Annotations:

  • Use @SpringBootTest and @ExtendWith(SpringExtension.class) with JUnit 5 for integration testing.
  • When @SpringBootTest is applied, it implicitly includes @ExtendWith(SpringExtension.class).

Context Loading:

  • @SpringBootTest loads the full Spring application context.
  • Enables Spring integration with JUnit 5, creating a testing environment with a fully configured Spring application context.

JUnit 5 Compatibility:

  • Replaces the usage of @RunWith(SpringRunner.class) when utilizing JUnit 5.

Testing Environment:

  • Tests the application as if it were running in a real environment.
  • Suitable for end-to-end testing, ensuring that various components work together seamlessly.

Code Coverage:

  • Offers higher code coverage as it exercises the entire application stack.

Context Management:

  • Sets up the Spring context before test methods are executed and closes it afterward.

If you are using both Spring and Mockito in the same test class ensure that you initialize Mockito annotations using MockitoAnnotations.openMocks(this) in the @BeforeEach method to correctly set up the mocks.

@SpringBootTest // Load the Spring Boot application context
//@SpringBootTest(classes = BigQueryTestConfiguration.class)
@ExtendWith(SpringExtension.class) // Enable Spring integration with JUnit 5
// When @SpringBootTest is used, it implicitly includes @ExtendWith(SpringExtension.class)
class MyIntegrationTest {
}

JUnit 5 Annotations:

Test Annotation:

  • @Test : Identifies a method as a test method.
   @Test
   void myTestMethod() {
       // Test logic
   }

Lifecycle Annotations:

  • @BeforeAll : Denotes a method that should be run before all tests in a class.
  • @BeforeEach: Denotes a method that should be run before each test method.
  • @AfterEach : Denotes a method that should be run after each test method.
  • @AfterAll : Denotes a method that should be run after all tests in a class.

Test Assertion Annotations:

Assertions : Class for multiple assertion annotations like @assertTrue, @assertFalse, etc.

@Test
void testAssertions() {
    assertTrue(true, "Assertion message for true condition");
    assertFalse(false, "Assertion message for false condition");
    assertEquals(expected, actual, "The values are not equal!");//The message will be printed when the assertion fails
    IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, () -> {
        calculator.add(value, 4);
}

Test exceptions

// Call the method under test
FutureException exception = org.junit.jupiter.api.Assertions.assertThrows(FutureException.class, () -> {
   myQueryService.getOptionsData(anyString());
   });

Parameterized Tests:

  • @ParameterizedTest : Denotes that the annotated method is a parameterized test.
  • @ValueSource : Provides a single value for a parameterized test.
  • @CsvSource : Provides CSV-formatted values for a parameterized test.
  • @NullSource : pass a null value

@RepeatedTest : Indicates that the annotated method is a repeated test.

@RepeatedTest(3)
void repeatedTest() {
    // Test logic to be repeated 3 times
}

Conditional Test Execution:

  • @Disabled : Disables a test class or method.
@Test
@Disabled("Not implemented yet")
void disabledTest() {
    // Test logic (disabled)
}

Tagging and Filtering:

  • @Tag : Allows tagging tests for later filtering.
  • @DisplayName : Defines a custom display name for a test class or method.

Mockito Annotations:

Mocking Annotations:

  • @Mock : Creates a mock object.
  • @Spy : Creates a spy (partial mock) object.
    • The real methods of the object are invoked unless they are explicitly stubbed.

Mock the response to 2 DB Calls

Verification Annotations:

  • @MockitoSettings : Provides additional settings for Mockito.

@MockitoSettings(strictness = Strictness.LENIENT)

  • Mockito allows leniency regarding stubbed methods that are not explicitly invoked during the test.
  • The key point is that Mockito won’t enforce strict verification of interactions with the mock.
@MockitoSettings(strictness = Strictness.LENIENT)//Can be applies at class level as well
@Test
void lenientMockingTest() {
    when(someDependency.someMethod()).thenReturn("Mocked result");
    String result = someDependency.someMethod();
    assertEquals("Mocked result", result, "Lenient mocking test failed");
}
  • VerificationMode : Configures the verification mode (times, atLeastOnce, etc.).
  • @Captor : Captures argument values for further assertions.
@Captor
private ArgumentCaptor<String> stringCaptor;

Spring Testing Annotations:

Integration Testing:

  • @SpringBootTest : Loads the Spring application context for integration tests.
  • @DataJpaTest : Configures a test for JPA-based tests.

Dependency Injection:

  • @Autowired : Injects a bean into a test class or method.
  • @MockBean : Mocks a bean when used with @SpringBootTest.

Transaction Management:

  • @Transactional : Specifies that a test method should be run within a transaction.
@Transactional
@Test
void transactionalTest() {
    // Test logic within a transaction
}

Web Testing: Testing a Controller

  • @WebMvcTest : Configures a test for Spring MVC-based tests.
@WebMvcTest(MyController.class)
class MyControllerTest {
    // Web testing logic
}

Testing Components:

  • @ComponentScan : Configures component scanning for the test context.
@ComponentScan(basePackages = "com.example")
class MyComponentScanTest {
    // Test logic with custom component scanning
}

Profile Configuration:

  • @ActiveProfiles : Specifies which bean definition profiles should be active.
@ActiveProfiles("test")
class MyProfileTest {
    // Test logic with the "test" profile active
}

Property Source Configuration:

-@TestPropertySource : Configures properties for the test context.

@TestPropertySource(locations = "classpath:test.properties")
class MyPropertySourceTest {
    // Test logic with properties from test.properties
}

Test Static Methods

With Junit 3+ Static methods can be tested with

MockedStatic<UuidUtils> utilities = Mockito.mockStatic(UuidUtils.class)

Advanced Mocking Techniques

Stubbing Consecutive Calls

when(mockService.getData())
    .thenReturn("first call")
    .thenReturn("second call")
    .thenThrow(new RuntimeException("third call"));

Stubbing with Callbacks

when(mockService.processData(anyString()))
    .thenAnswer(invocation -> {
        String arg = invocation.getArgument(0);
        return "Processed: " + arg;
    });

Partial Mocking with Spies

@Spy
MyService spyService = new MyService();

@Test
void testPartialMocking() {
    // Real methods are called unless stubbed
    doReturn("mocked").when(spyService).expensiveOperation();
    
    String result = spyService.businessMethod();
    
    verify(spyService).expensiveOperation();
}

Custom Argument Matchers

// Create custom matcher
ArgumentMatcher<User> userMatcher = user -> 
    user != null && "admin".equals(user.getRole());

when(userService.processUser(argThat(userMatcher)))
    .thenReturn("Admin processed");

Argument Matchers

  • anyString() and any() are argument matchers in Mockito.
  • any() - be generic always, by passing all the arguments as arg matchers or be specific
  • They are more lenient, allowing matching for any argument of the specified type.
  • @MockitoSettings(strictness = Strictness.LENIENT):
    • Configures the strictness level of Mockito.
    • In lenient mode, Mockito is more permissive with interactions, allowing non-stubbed method calls.

Use Argument matchers only on the Mocks

 // Mocking the behavior of reportsBigQueryService
List<Map<String, Object>> mockDBCall = dataAccessObjectMock.getRecordFromView(anyString(), anyString(), any());
when(mockDBCall).thenReturn(data);//mockDBCall == data initialized in @BeforeEach void setUp()

DO NOT use the argument Matchers on SUT, or injectableMocks and Do no mix hardcoded values with arg matchers

// Call the method under test
List<Map<String, Object>> result = yourServiceUnderTest.getDataById(anyString(), "sample", "facility");

//Logs
org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!

Mock Dummy Data

Create with Dummy data

Create from a json file

Test Data Builders

public class UserTestDataBuilder {
    private String name = "John Doe";
    private String email = "john@example.com";
    private String role = "USER";
    
    public UserTestDataBuilder withName(String name) {
        this.name = name;
        return this;
    }
    
    public UserTestDataBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
    
    public UserTestDataBuilder withRole(String role) {
        this.role = role;
        return this;
    }
    
    public User build() {
        return new User(name, email, role);
    }
}

// Usage in tests
@Test
void testUserCreation() {
    User user = new UserTestDataBuilder()
        .withName("Alice")
        .withRole("ADMIN")
        .build();
        
    // Test logic here
}

Testing Best Practices

The AAA Pattern

Structure your tests using Arrange-Act-Assert:

@Test
void shouldCalculateDiscountForPremiumUser() {
    // Arrange
    User premiumUser = new UserTestDataBuilder()
        .withRole("PREMIUM")
        .build();
    when(userService.findById(1L)).thenReturn(premiumUser);
    
    // Act
    BigDecimal discount = discountService.calculateDiscount(1L, new BigDecimal("100"));
    
    // Assert
    assertThat(discount).isEqualByComparingTo(new BigDecimal("10.00"));
    verify(userService).findById(1L);
}

Test Naming Conventions

// Good naming patterns:
@Test
void shouldReturnEmptyListWhenNoUsersExist() { }

@Test
void shouldThrowExceptionWhenUserIdIsNull() { }

@Test
void givenInvalidEmail_whenValidatingUser_thenThrowValidationException() { }

Mockito Verification Modes

// Verify exact number of calls
verify(mockService, times(3)).getData();

// Verify at least/at most
verify(mockService, atLeast(1)).getData();
verify(mockService, atMost(5)).getData();

// Verify no interactions
verifyNoInteractions(mockService);

// Verify no more interactions after specified ones
verify(mockService).getData();
verifyNoMoreInteractions(mockService);

// Verify in order
InOrder inOrder = inOrder(mockService1, mockService2);
inOrder.verify(mockService1).methodA();
inOrder.verify(mockService2).methodB();

Debugging

Get the JSON Response from IntelliJ Debugger (after the DB Call) or from service request. Use breakpoint to evaluate expression

new com.fasterxml.jackson.databind.ObjectMapper()
        .registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule())
        .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 
        .writerWithDefaultPrettyPrinter() 
        .writeValueAsString(data);

# Modern Testing Approaches

## TestContainers for Integration Testing

```java
@SpringBootTest
@Testcontainers
class DatabaseIntegrationTest {
    
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    
    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Test
    void shouldPersistUser() {
        // Test with real database
    }
}

AssertJ for Fluent Assertions

// Instead of JUnit assertions, use AssertJ for better readability
import static org.assertj.core.api.Assertions.*;

@Test
void shouldReturnFilteredUsers() {
    List<User> users = userService.getActiveUsers();
    
    assertThat(users)
        .isNotEmpty()
        .hasSize(3)
        .extracting(User::getName)
        .containsExactly("Alice", "Bob", "Charlie")
        .doesNotContain("Inactive User");
}

Testing Async Code

@Test
void shouldHandleAsyncOperation() throws Exception {
    CompletableFuture<String> future = asyncService.processAsync("data");
    
    // Test async completion
    assertThat(future).succeedsWithin(Duration.ofSeconds(5))
                     .isEqualTo("Processed: data");
}

// Testing with @Async methods
@Test
void shouldProcessAsyncMethod() {
    asyncService.processInBackground("test");
    
    // Use Awaitility for async verification
    await().atMost(2, SECONDS)
           .untilAsserted(() -> 
               verify(mockService).save(argThat(data -> 
                   "test".equals(data.getValue()))));
}

Common Testing Pitfalls to Avoid

1. Over-Mocking

// Bad - mocking everything
@Mock List<String> mockList;
@Mock String mockString;

// Good - mock only external dependencies
@Mock UserRepository userRepository;

2. Testing Implementation Details

// Bad - testing internal method calls
verify(userService).validateUser(user);
verify(userService).encryptPassword(user);
verify(userService).saveToDatabase(user);

// Good - testing behavior
User result = userService.createUser(userData);
assertThat(result.getId()).isNotNull();

3. Brittle Tests

// Bad - depending on exact order or timing
verify(mockService, times(1)).method1();
verify(mockService, times(1)).method2();

// Good - verify what matters
verify(mockService).saveUser(any(User.class));

Test Coverage and Quality

JaCoCo Configuration

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.7</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Test Quality Guidelines

  • F.I.R.S.T Principles:
    • Fast: Tests should run quickly
    • Independent: Tests should not depend on each other
    • Repeatable: Tests should be consistent
    • Self-Validating: Tests should have boolean output
    • Timely: Write tests before or with the code
  • Test Pyramid: More unit tests, fewer integration tests, minimal E2E tests
  • Aim for 80-90% code coverage, but focus on meaningful tests
  • Test edge cases and error conditions
  • Keep tests simple and focused on one thing ```