Comprehensive Guide to JUnit and Mockito Testing
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
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()
andany()
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 ```