🧪 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 equalassertTrue(condition)
: Checks if a condition is trueassertFalse(condition)
: Checks if a condition is falseassertNull(object)
: Checks if an object is nullassertNotNull(object)
: Checks if an object is not nullassertThrows(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 classwhen(methodCall).thenReturn(value)
: Defines behavior for a method callwhen(methodCall).thenThrow(exception)
: Makes a method throw an exceptionverify(mock).method()
: Verifies a method was called on the mockverify(mock, times(n)).method()
: Verifies a method was called n timesverify(mock, never()).method()
: Verifies a method was never called
🔄 Test Doubles with Mockito
Mockito provides different types of test doubles:
- Mocks: Objects that record method calls for verification
List<String> mockedList = mock(List.class);
mockedList.add("one");
verify(mockedList).add("one");
- 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));
- 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 objectanyInt()
,anyString()
, etc.: Type-specific matcherseq(value)
: Matches a specific valuecontains(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:
- Write a failing test that defines the expected behavior
- Implement the minimum code to make the test pass
- 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
-
Keep tests simple and focused
- Each test should verify one specific behavior
- Use descriptive test names that explain what's being tested
-
Follow the AAA pattern
- Arrange: Set up test objects and mocks
- Act: Call the method being tested
- Assert: Verify the expected outcome
-
Use appropriate assertions
- Choose the most specific assertion for your case
- Include meaningful error messages in assertions
-
Mock only what's necessary
- Only mock external dependencies or complex objects
- Use real objects for simple dependencies
-
Organize tests logically
- Group related tests in the same class
- Use nested classes for different test scenarios
-
Reset mocks between tests
- Use
@BeforeEach
to reset or recreate mocks - Or use
@ExtendWith(MockitoExtension.class)
with@Mock
annotations
- Use
-
Test both happy paths and edge cases
- Verify normal operation works correctly
- Test boundary conditions and error handling
❌ Don'ts
-
Don't mock everything
- Excessive mocking leads to brittle tests
- Tests should verify behavior, not implementation details
-
Avoid complex test setup
- If setup is complex, your design might need improvement
- Extract common setup into helper methods
-
Don't test trivial code
- Skip testing simple getters/setters
- Focus on code with logic or business rules
-
Don't use mocks for value objects
- Create real instances of simple objects
- Mock only objects with behavior
-
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:
User
class with username, password, and role fieldsUserRepository
interface with methods to find and save usersAuthenticationService
class that depends onUserRepository
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:
Product
class with id, name, and priceDiscountService
interface that calculates discounts based on products and quantitiesShoppingCart
class that usesDiscountService
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