Testing in Java with JUnit and Mockito
Automated tests are fundamental to ensure code quality and maintainability. JUnit is the standard framework for unit testing in Java, while Mockito allows creating mock objects.
What You'll Learn
- JUnit 5: annotations and assertions
- Parameterized tests
- Mockito: mock, stub and verify
- Test-Driven Development (TDD)
- Testing best practices
JUnit 5 - Fundamentals
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc;
@BeforeEach
void setUp() {
calc = new Calculator();
}
@Test
@DisplayName("Addition of two positive numbers")
void testAddition() {
assertEquals(5, calc.add(2, 3));
}
@Test
void testSubtraction() {
assertEquals(2, calc.subtract(5, 3));
}
@Test
void testDivisionByZero() {
assertThrows(ArithmeticException.class, () -> {
calc.divide(10, 0);
});
}
@Test
@Disabled("Test temporarily disabled")
void testToImplement() {
fail("Not yet implemented");
}
}
class Calculator {
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int divide(int a, int b) { return a / b; }
}
JUnit 5 Annotations
| Annotation | Description |
|---|---|
| @Test | Marks a method as a test |
| @BeforeEach | Executed before each test |
| @AfterEach | Executed after each test |
| @BeforeAll | Executed once before all tests |
| @DisplayName | Human-readable test name |
| @Disabled | Temporarily disables the test |
Advanced Assertions
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.*;
class AssertionsTest {
@Test
void basicAssertions() {
// Equality
assertEquals(4, 2 + 2);
assertNotEquals(5, 2 + 2);
// Booleans
assertTrue(3 > 2);
assertFalse(2 > 3);
// Null
Object obj = null;
assertNull(obj);
assertNotNull(new Object());
// Same (same instance)
String s1 = "hello";
String s2 = s1;
assertSame(s1, s2);
}
@Test
void collectionAssertions() {
List<String> list = Arrays.asList("A", "B", "C");
assertEquals(3, list.size());
assertTrue(list.contains("B"));
// Arrays
int[] expected = {1, 2, 3};
int[] actual = {1, 2, 3};
assertArrayEquals(expected, actual);
}
@Test
void exceptionAssertions() {
Exception ex = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("Error message");
});
assertEquals("Error message", ex.getMessage());
}
@Test
void timeoutAssertions() {
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(500); // Must complete within 2 seconds
});
}
@Test
void groupedAssertions() {
Student s = new Student("John", 25);
assertAll("Verify student",
() -> assertEquals("John", s.getName()),
() -> assertEquals(25, s.getAge()),
() -> assertTrue(s.getAge() > 0)
);
}
}
class Student {
private String name;
private int age;
Student(String name, int age) { this.name = name; this.age = age; }
String getName() { return name; }
int getAge() { return age; }
}
Parameterized Tests
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class ParameterizedTestsDemo {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testPositiveNumbers(int number) {
assertTrue(number > 0);
}
@ParameterizedTest
@ValueSource(strings = {"hello", "world", "java"})
void testNonEmptyStrings(String str) {
assertFalse(str.isEmpty());
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, 20, 30"
})
void testSum(int a, int b, int result) {
assertEquals(result, a + b);
}
@ParameterizedTest
@MethodSource("provideNumbers")
void testWithMethodSource(int number, boolean expected) {
assertEquals(expected, number % 2 == 0);
}
static Stream<Arguments> provideNumbers() {
return Stream.of(
Arguments.of(2, true),
Arguments.of(3, false),
Arguments.of(4, true)
);
}
@ParameterizedTest
@EnumSource(Month.class)
void testMonths(Month month) {
assertNotNull(month);
}
enum Month { JANUARY, FEBRUARY, MARCH }
}
Mockito - Mock Objects
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
// Interface to mock
interface UserRepository {
User findById(Long id);
void save(User user);
List<User> findAll();
}
class User {
private Long id;
private String name;
User(Long id, String name) {
this.id = id;
this.name = name;
}
Long getId() { return id; }
String getName() { return name; }
}
class UserService {
private final UserRepository repository;
UserService(UserRepository repository) {
this.repository = repository;
}
User getUser(Long id) {
return repository.findById(id);
}
void createUser(User user) {
repository.save(user);
}
}
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository mockRepository;
@InjectMocks
private UserService userService;
@Test
void testGetUser() {
// Arrange: configure the mock
User expectedUser = new User(1L, "John");
when(mockRepository.findById(1L)).thenReturn(expectedUser);
// Act: execute the method
User result = userService.getUser(1L);
// Assert: verify
assertEquals("John", result.getName());
verify(mockRepository).findById(1L);
}
@Test
void testCreateUser() {
User newUser = new User(2L, "Jane");
userService.createUser(newUser);
// Verify save was called
verify(mockRepository, times(1)).save(newUser);
}
@Test
void testUserNotFound() {
when(mockRepository.findById(99L)).thenReturn(null);
User result = userService.getUser(99L);
assertNull(result);
}
}
Advanced Mockito
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class AdvancedMockitoTest {
@Test
void testAdvancedStub() {
List<String> mockList = mock(List.class);
// Stub with any matcher
when(mockList.get(anyInt())).thenReturn("element");
assertEquals("element", mockList.get(5));
// Stub with different behavior
when(mockList.size())
.thenReturn(1)
.thenReturn(2)
.thenReturn(3);
assertEquals(1, mockList.size()); // First call
assertEquals(2, mockList.size()); // Second
assertEquals(3, mockList.size()); // Third and onwards
// Stub that throws exception
when(mockList.get(100)).thenThrow(new IndexOutOfBoundsException());
}
@Test
void testAdvancedVerify() {
List<String> mockList = mock(List.class);
mockList.add("one");
mockList.add("two");
mockList.add("one");
// Verify call count
verify(mockList, times(2)).add("one");
verify(mockList, times(1)).add("two");
verify(mockList, never()).add("three");
verify(mockList, atLeast(1)).add(anyString());
verify(mockList, atMost(3)).add(anyString());
}
@Test
void testArgumentCaptor() {
UserRepository mockRepo = mock(UserRepository.class);
UserService service = new UserService(mockRepo);
// Capture argument passed to save
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
service.createUser(new User(1L, "Test"));
verify(mockRepo).save(captor.capture());
User capturedUser = captor.getValue();
assertEquals("Test", capturedUser.getName());
}
@Test
void testPartialSpy() {
// Spy: real object with some parts mocked
List<String> realList = new ArrayList<>();
List<String> spyList = spy(realList);
spyList.add("element"); // Calls real method
assertEquals(1, spyList.size());
// Override a specific method
doReturn(100).when(spyList).size();
assertEquals(100, spyList.size());
}
}
Test-Driven Development (TDD)
// TDD follows the cycle: RED -> GREEN -> REFACTOR
// STEP 1 - RED: Write the test BEFORE the code
class ShoppingCartTest {
@Test
void emptyCartHasZeroTotal() {
ShoppingCart cart = new ShoppingCart();
assertEquals(0.0, cart.getTotal(), 0.001);
}
@Test
void addProductCalculatesTotal() {
ShoppingCart cart = new ShoppingCart();
Product p = new Product("Book", 29.99);
cart.add(p);
assertEquals(29.99, cart.getTotal(), 0.001);
}
@Test
void apply10PercentDiscount() {
ShoppingCart cart = new ShoppingCart();
cart.add(new Product("A", 100.0));
cart.applyDiscount(10);
assertEquals(90.0, cart.getTotal(), 0.001);
}
}
// STEP 2 - GREEN: Write minimum code to make the test pass
class Product {
String name;
double price;
Product(String name, double price) {
this.name = name;
this.price = price;
}
}
class ShoppingCart {
private List<Product> products = new ArrayList<>();
private double discountPercentage = 0;
void add(Product p) {
products.add(p);
}
double getTotal() {
double total = products.stream()
.mapToDouble(p -> p.price)
.sum();
return total * (1 - discountPercentage / 100);
}
void applyDiscount(double percentage) {
this.discountPercentage = percentage;
}
}
// STEP 3 - REFACTOR: Improve the code keeping tests green
Best Practices
Rules for Effective Tests
- One logical assertion per test: focused tests
- Descriptive names: describe what they test
- AAA pattern: Arrange, Act, Assert
- Independent tests: don't depend on order
- Fast tests: must be run often
- Mock only external dependencies: database, API, files
- Avoid fragile tests: don't test implementation
- Coverage isn't everything: quality > quantity







