JUnit is the most popular testing framework for Java applications. It provides annotations and assertions to write and run repeatable tests.
1. Setup and Dependencies
Maven Dependencies
<!-- pom.xml --> <dependencies> <!-- JUnit 5 (Jupiter) --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency> <!-- AssertJ for fluent assertions --> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.2</version> <scope>test</scope> </dependency> <!-- Mockito for mocking --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.5.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.5.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.0</version> </plugin> </plugins> </build>
Gradle Dependencies
// build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation 'org.mockito:mockito-core:5.5.0'
testImplementation 'org.mockito:mockito-junit-jupiter:5.5.0'
}
test {
useJUnitPlatform()
}
2. Basic JUnit Testing
Simple Test Class
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
// Test class naming convention: ClassNameTest
class CalculatorTest {
private Calculator calculator;
// Setup method - runs before each test
@BeforeEach
void setUp() {
calculator = new Calculator();
System.out.println("Setting up before test");
}
// Cleanup method - runs after each test
@AfterEach
void tearDown() {
calculator = null;
System.out.println("Cleaning up after test");
}
// Test method
@Test
void testAddition() {
// Given
int a = 5;
int b = 3;
// When
int result = calculator.add(a, b);
// Then
assertEquals(8, result, "5 + 3 should equal 8");
}
@Test
void testSubtraction() {
assertEquals(2, calculator.subtract(5, 3));
assertEquals(-2, calculator.subtract(3, 5));
}
@Test
void testMultiplication() {
assertEquals(15, calculator.multiply(5, 3));
assertEquals(0, calculator.multiply(5, 0));
}
@Test
void testDivision() {
assertEquals(2, calculator.divide(6, 3));
assertEquals(2.5, calculator.divide(5, 2), 0.001);
}
@Test
void testDivisionByZero() {
// Test that exception is thrown
Exception exception = assertThrows(ArithmeticException.class,
() -> calculator.divide(5, 0));
assertEquals("Division by zero is not allowed", exception.getMessage());
}
// Display names for better test reports
@Test
@DisplayName("Addition with positive numbers")
void additionWithPositiveNumbers() {
assertAll("Multiple addition assertions",
() -> assertEquals(10, calculator.add(7, 3)),
() -> assertEquals(15, calculator.add(10, 5)),
() -> assertEquals(0, calculator.add(0, 0))
);
}
}
// Class under test
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
public double divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Division by zero is not allowed");
}
return (double) a / b;
}
}
3. Test Annotations and Lifecycle
Test Lifecycle Annotations
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class LifecycleTest {
// Runs once before all tests in the class
@BeforeAll
static void setUpClass() {
System.out.println("BeforeAll - Setting up test class");
}
// Runs once after all tests in the class
@AfterAll
static void tearDownClass() {
System.out.println("AfterAll - Cleaning up test class");
}
// Runs before each test method
@BeforeEach
void setUp() {
System.out.println("BeforeEach - Setting up test");
}
// Runs after each test method
@AfterEach
void tearDown() {
System.out.println("AfterEach - Cleaning up test");
}
@Test
void testOne() {
System.out.println("Running testOne");
assertTrue(true);
}
@Test
void testTwo() {
System.out.println("Running testTwo");
assertFalse(false);
}
// Disabled test
@Test
@Disabled("This test is not implemented yet")
void testDisabled() {
fail("This test should be disabled");
}
// Test with timeout
@Test
@Timeout(5) // 5 seconds
void testWithTimeout() throws InterruptedException {
Thread.sleep(3000); // This should complete within 5 seconds
assertTrue(true);
}
}
Parameterized Tests
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class ParameterizedCalculatorTest {
private Calculator calculator = new Calculator();
// Value Source - simple values
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testSquareWithValueSource(int number) {
int expected = number * number;
assertEquals(expected, calculator.square(number));
}
// Method Source - complex values from a method
@ParameterizedTest
@MethodSource("additionProvider")
void testAdditionWithMethodSource(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
private static Stream<Arguments> additionProvider() {
return Stream.of(
Arguments.of(1, 1, 2),
Arguments.of(2, 3, 5),
Arguments.of(-1, 1, 0),
Arguments.of(0, 0, 0)
);
}
// CSV Source - comma-separated values
@ParameterizedTest
@CsvSource({
"2, 3, 6",
"5, 0, 0",
"-2, 4, -8",
"10, 10, 100"
})
void testMultiplicationWithCsvSource(int a, int b, int expected) {
assertEquals(expected, calculator.multiply(a, b));
}
// CSV File Source - values from CSV file
@ParameterizedTest
@CsvFileSource(resources = "/division_test_data.csv", numLinesToSkip = 1)
void testDivisionWithCsvFile(int dividend, int divisor, double expected) {
assertEquals(expected, calculator.divide(dividend, divisor), 0.001);
}
// Enum Source
@ParameterizedTest
@EnumSource(Operation.class)
void testEnumOperations(Operation operation) {
assertNotNull(operation);
assertTrue(operation.name().length() > 0);
}
}
// Add this method to Calculator class
class Calculator {
// ... previous methods ...
public int square(int number) {
return number * number;
}
}
enum Operation {
ADD, SUBTRACT, MULTIPLY, DIVIDE
}
Test Data File
# src/test/resources/division_test_data.csv dividend,divisor,expected 10,2,5.0 15,3,5.0 7,2,3.5 100,10,10.0
4. Advanced Assertions
Using AssertJ for Fluent Assertions
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class AssertJTest {
@Test
void testStringAssertions() {
String name = "John Doe";
assertThat(name)
.isNotNull()
.isNotEmpty()
.hasSize(8)
.contains("John")
.doesNotContain("Jane")
.startsWith("John")
.endsWith("Doe")
.isEqualTo("John Doe")
.isEqualToIgnoringCase("john doe");
}
@Test
void testListAssertions() {
List<String> fruits = Arrays.asList("apple", "banana", "cherry");
assertThat(fruits)
.isNotNull()
.hasSize(3)
.contains("banana")
.doesNotContain("orange")
.containsExactly("apple", "banana", "cherry")
.containsExactlyInAnyOrder("cherry", "apple", "banana")
.allMatch(fruit -> fruit.length() > 3)
.noneMatch(fruit -> fruit.isEmpty());
}
@Test
void testMapAssertions() {
Map<String, Integer> scores = Map.of(
"John", 85,
"Jane", 92,
"Bob", 78
);
assertThat(scores)
.hasSize(3)
.containsKeys("John", "Jane")
.doesNotContainKeys("Alice")
.containsEntry("John", 85)
.allSatisfy((name, score) -> {
assertThat(name).isNotBlank();
assertThat(score).isBetween(0, 100);
});
}
@Test
void testExceptionAssertions() {
Calculator calculator = new Calculator();
assertThatThrownBy(() -> calculator.divide(10, 0))
.isInstanceOf(ArithmeticException.class)
.hasMessage("Division by zero is not allowed")
.hasNoCause();
assertThatExceptionOfType(ArithmeticException.class)
.isThrownBy(() -> calculator.divide(5, 0))
.withMessage("Division by zero is not allowed");
}
@Test
void testOptionalAssertions() {
Optional<String> present = Optional.of("value");
Optional<String> empty = Optional.empty();
assertThat(present)
.isPresent()
.contains("value")
.hasValue("value");
assertThat(empty).isEmpty();
}
@Test
void testObjectAssertions() {
Person person = new Person("John", 30, "[email protected]");
assertThat(person)
.isNotNull()
.hasFieldOrProperty("name")
.hasFieldOrPropertyWithValue("age", 30)
.extracting(Person::getName, Person::getEmail)
.containsExactly("John", "[email protected]");
}
}
class Person {
private String name;
private int age;
private String email;
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
}
Hamcrest Matchers (Alternative)
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
class HamcrestTest {
@Test
void testHamcrestMatchers() {
String text = "Hello World";
List<String> items = Arrays.asList("one", "two", "three");
assertThat(text, is(notNullValue()));
assertThat(text, equalTo("Hello World"));
assertThat(text, containsString("World"));
assertThat(text, startsWith("Hello"));
assertThat(text, endsWith("World"));
assertThat(items, hasSize(3));
assertThat(items, hasItem("two"));
assertThat(items, contains("one", "two", "three"));
assertThat(items, everyItem(hasLength(greaterThan(2))));
assertThat(15.5, closeTo(15.0, 0.6));
assertThat(10, greaterThan(5));
assertThat(10, lessThanOrEqualTo(10));
}
}
5. Mocking with Mockito
Basic Mocking
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
// Enable Mockito annotations
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void testGetUserById() {
// Given
User expectedUser = new User(1L, "[email protected]", "John Doe");
when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
// When
Optional<User> result = userService.getUserById(1L);
// Then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("John Doe");
verify(userRepository).findById(1L);
}
@Test
void testGetUserByIdNotFound() {
// Given
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// When
Optional<User> result = userService.getUserById(999L);
// Then
assertThat(result).isEmpty();
verify(userRepository).findById(999L);
}
@Test
void testCreateUser() {
// Given
User newUser = new User(null, "[email protected]", "Jane Smith");
User savedUser = new User(1L, "[email protected]", "Jane Smith");
when(userRepository.save(newUser)).thenReturn(savedUser);
doNothing().when(emailService).sendWelcomeEmail(anyString());
// When
User result = userService.createUser(newUser);
// Then
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("Jane Smith");
verify(userRepository).save(newUser);
verify(emailService).sendWelcomeEmail("[email protected]");
}
@Test
void testGetAllUsers() {
// Given
List<User> users = Arrays.asList(
new User(1L, "[email protected]", "John Doe"),
new User(2L, "[email protected]", "Jane Smith")
);
when(userRepository.findAll()).thenReturn(users);
// When
List<User> result = userService.getAllUsers();
// Then
assertThat(result).hasSize(2);
assertThat(result).extracting(User::getName)
.containsExactly("John Doe", "Jane Smith");
verify(userRepository).findAll();
}
@Test
void testDeleteUser() {
// Given
Long userId = 1L;
when(userRepository.existsById(userId)).thenReturn(true);
doNothing().when(userRepository).deleteById(userId);
// When
boolean result = userService.deleteUser(userId);
// Then
assertThat(result).isTrue();
verify(userRepository).existsById(userId);
verify(userRepository).deleteById(userId);
}
@Test
void testDeleteUserNotFound() {
// Given
Long userId = 999L;
when(userRepository.existsById(userId)).thenReturn(false);
// When
boolean result = userService.deleteUser(userId);
// Then
assertThat(result).isFalse();
verify(userRepository).existsById(userId);
verify(userRepository, never()).deleteById(anyLong());
}
@Test
void testArgumentMatchers() {
User user = new User(1L, "[email protected]", "Test User");
when(userRepository.findByEmail(anyString())).thenReturn(Optional.of(user));
when(userRepository.findByEmail(startsWith("admin"))).thenReturn(Optional.empty());
Optional<User> result1 = userService.findByEmail("[email protected]");
Optional<User> result2 = userService.findByEmail("[email protected]");
assertThat(result1).isPresent();
assertThat(result2).isEmpty();
verify(userRepository, times(2)).findByEmail(anyString());
}
}
// Service classes under test
class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
public User createUser(User user) {
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser.getEmail());
return savedUser;
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public boolean deleteUser(Long id) {
if (userRepository.existsById(id)) {
userRepository.deleteById(id);
return true;
}
return false;
}
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
}
interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
List<User> findAll();
boolean existsById(Long id);
void deleteById(Long id);
Optional<User> findByEmail(String email);
}
interface EmailService {
void sendWelcomeEmail(String email);
}
class User {
private Long id;
private String email;
private String name;
public User(Long id, String email, String name) {
this.id = id;
this.email = email;
this.name = name;
}
// Getters
public Long getId() { return id; }
public String getEmail() { return email; }
public String getName() { return name; }
}
Advanced Mocking
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentService paymentService;
@Mock
private InventoryService inventoryService;
@InjectMocks
private OrderService orderService;
@Test
void testProcessOrderSuccess() {
// Given
Order order = new Order(1L, 100.0, "PENDING");
when(inventoryService.checkAvailability(any(Order.class))).thenReturn(true);
when(paymentService.processPayment(any(Order.class))).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenReturn(order);
// When
boolean result = orderService.processOrder(order);
// Then
assertThat(result).isTrue();
assertThat(order.getStatus()).isEqualTo("COMPLETED");
verify(inventoryService).checkAvailability(order);
verify(paymentService).processPayment(order);
verify(orderRepository).save(order);
}
@Test
void testProcessOrderInventoryUnavailable() {
// Given
Order order = new Order(1L, 100.0, "PENDING");
when(inventoryService.checkAvailability(any(Order.class))).thenReturn(false);
// When
boolean result = orderService.processOrder(order);
// Then
assertThat(result).isFalse();
assertThat(order.getStatus()).isEqualTo("CANCELLED");
verify(inventoryService).checkAvailability(order);
verify(paymentService, never()).processPayment(any(Order.class));
verify(orderRepository).save(order);
}
@Test
void testProcessOrderPaymentFailed() {
// Given
Order order = new Order(1L, 100.0, "PENDING");
when(inventoryService.checkAvailability(any(Order.class))).thenReturn(true);
when(paymentService.processPayment(any(Order.class))).thenReturn(false);
// When
boolean result = orderService.processOrder(order);
// Then
assertThat(result).isFalse();
assertThat(order.getStatus()).isEqualTo("FAILED");
verify(inventoryService).checkAvailability(order);
verify(paymentService).processPayment(order);
verify(orderRepository).save(order);
}
@Test
void testVerifyInteractionOrder() {
Order order = new Order(1L, 100.0, "PENDING");
when(inventoryService.checkAvailability(any(Order.class))).thenReturn(true);
when(paymentService.processPayment(any(Order.class))).thenReturn(true);
when(orderRepository.save(any(Order.class))).thenReturn(order);
orderService.processOrder(order);
// Verify order of interactions
inOrder(inventoryService, paymentService, orderRepository).verify(inventoryService).checkAvailability(order);
inOrder(inventoryService, paymentService, orderRepository).verify(paymentService).processPayment(order);
inOrder(inventoryService, paymentService, orderRepository).verify(orderRepository).save(order);
}
@Test
void testSpyExample() {
List<String> list = Arrays.asList("one", "two", "three");
List<String> spyList = spy(list);
// You can stub specific methods
when(spyList.size()).thenReturn(100);
// But other methods work as normal
assertThat(spyList.get(0)).isEqualTo("one");
assertThat(spyList.size()).isEqualTo(100);
verify(spyList).get(0);
verify(spyList).size();
}
}
// Supporting classes
class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(OrderRepository orderRepository, PaymentService paymentService,
InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public boolean processOrder(Order order) {
if (!inventoryService.checkAvailability(order)) {
order.setStatus("CANCELLED");
orderRepository.save(order);
return false;
}
if (!paymentService.processPayment(order)) {
order.setStatus("FAILED");
orderRepository.save(order);
return false;
}
order.setStatus("COMPLETED");
orderRepository.save(order);
return true;
}
}
interface OrderRepository {
Order save(Order order);
}
interface PaymentService {
boolean processPayment(Order order);
}
interface InventoryService {
boolean checkAvailability(Order order);
}
class Order {
private Long id;
private Double amount;
private String status;
private LocalDateTime createdAt;
public Order(Long id, Double amount, String status) {
this.id = id;
this.amount = amount;
this.status = status;
this.createdAt = LocalDateTime.now();
}
// Getters and setters
public Long getId() { return id; }
public Double getAmount() { return amount; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
6. Integration Testing
Spring Boot Integration Tests
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
// Spring Boot test with minimal configuration
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@MockBean
private EmailService emailService;
@Test
void contextLoads() {
// Verify that Spring context loads successfully
assertThat(userService).isNotNull();
}
@Test
void testCreateUserIntegration() {
// Given
User newUser = new User(null, "[email protected]", "Integration Test");
User savedUser = new User(1L, "[email protected]", "Integration Test");
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
User result = userService.createUser(newUser);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getEmail()).isEqualTo("[email protected]");
}
@Test
void testFindUserIntegration() {
// Given
User user = new User(1L, "[email protected]", "Find Test");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
// When
Optional<User> result = userService.getUserById(1L);
// Then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("Find Test");
}
}
Test Configuration
# src/test/resources/application-test.properties spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true logging.level.com.example=DEBUG
7. Test Organization and Best Practices
Nested Tests for Better Organization
import org.junit.jupiter.api.*;
import java.util.HashMap;
import java.util.Map;
import static org.assertj.core.api.Assertions.*;
@DisplayName("User Management System")
class UserManagementTest {
private Map<Long, User> userDatabase;
private UserService userService;
@BeforeEach
void setUp() {
userDatabase = new HashMap<>();
userService = new UserService(userDatabase);
}
@Nested
@DisplayName("When no users exist")
class NoUsers {
@Test
@DisplayName("Should return empty list")
void shouldReturnEmptyList() {
assertThat(userService.getAllUsers()).isEmpty();
}
@Test
@DisplayName("Should not find user by ID")
void shouldNotFindUserById() {
assertThat(userService.getUserById(1L)).isEmpty();
}
}
@Nested
@DisplayName("When users exist")
class UsersExist {
@BeforeEach
void setUp() {
userService.createUser(new User(null, "[email protected]", "User One"));
userService.createUser(new User(null, "[email protected]", "User Two"));
}
@Test
@DisplayName("Should return all users")
void shouldReturnAllUsers() {
assertThat(userService.getAllUsers()).hasSize(2);
}
@Test
@DisplayName("Should find user by ID")
void shouldFindUserById() {
Optional<User> user = userService.getUserById(1L);
assertThat(user).isPresent();
assertThat(user.get().getName()).isEqualTo("User One");
}
@Nested
@DisplayName("When updating users")
class UpdateUsers {
@Test
@DisplayName("Should update user email")
void shouldUpdateUserEmail() {
User updatedUser = new User(1L, "[email protected]", "User One");
userService.updateUser(updatedUser);
Optional<User> user = userService.getUserById(1L);
assertThat(user).isPresent();
assertThat(user.get().getEmail()).isEqualTo("[email protected]");
}
}
}
@Nested
@DisplayName("User validation")
class UserValidation {
@Test
@DisplayName("Should reject null email")
void shouldRejectNullEmail() {
User invalidUser = new User(null, null, "Name");
assertThatThrownBy(() -> userService.createUser(invalidUser))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Email cannot be null or empty");
}
@Test
@DisplayName("Should reject empty name")
void shouldRejectEmptyName() {
User invalidUser = new User(null, "[email protected]", "");
assertThatThrownBy(() -> userService.createUser(invalidUser))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Name cannot be null or empty");
}
}
}
// Enhanced UserService for testing
class UserService {
private final Map<Long, User> userDatabase;
private Long nextId = 1L;
public UserService(Map<Long, User> userDatabase) {
this.userDatabase = userDatabase;
}
public User createUser(User user) {
validateUser(user);
user.setId(nextId++);
userDatabase.put(user.getId(), user);
return user;
}
public Optional<User> getUserById(Long id) {
return Optional.ofNullable(userDatabase.get(id));
}
public void updateUser(User user) {
if (!userDatabase.containsKey(user.getId())) {
throw new IllegalArgumentException("User not found with id: " + user.getId());
}
validateUser(user);
userDatabase.put(user.getId(), user);
}
public java.util.List<User> getAllUsers() {
return new java.util.ArrayList<>(userDatabase.values());
}
private void validateUser(User user) {
if (user.getEmail() == null || user.getEmail().trim().isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (user.getName() == null || user.getName().trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
}
}
Test Templates and Repeated Tests
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class AdvancedTestExamples {
@RepeatedTest(5)
@DisplayName("Repeated test example")
void repeatedTest(RepetitionInfo repetitionInfo) {
System.out.println("Repetition " + repetitionInfo.getCurrentRepetition() +
" of " + repetitionInfo.getTotalRepetitions());
assertThat(repetitionInfo.getCurrentRepetition()).isPositive();
}
@TestFactory
Stream<DynamicTest> dynamicTests() {
return Stream.of(
DynamicTest.dynamicTest("First dynamic test", () -> {
assertThat(2 + 2).isEqualTo(4);
}),
DynamicTest.dynamicTest("Second dynamic test", () -> {
assertThat("hello".toUpperCase()).isEqualTo("HELLO");
}),
DynamicTest.dynamicTest("Third dynamic test", () -> {
assertThat(true).isTrue();
})
);
}
@ParameterizedTest
@MethodSource("provideTestData")
void testWithMultipleParameters(int input, int expected, String description) {
Calculator calculator = new Calculator();
assertThat(calculator.square(input)).isEqualTo(expected);
}
private static Stream<Arguments> provideTestData() {
return Stream.of(
Arguments.of(1, 1, "Square of 1"),
Arguments.of(2, 4, "Square of 2"),
Arguments.of(3, 9, "Square of 3"),
Arguments.of(4, 16, "Square of 4"),
Arguments.of(5, 25, "Square of 5")
);
}
}
8. Best Practices Summary
Testing Best Practices
/**
* TESTING BEST PRACTICES:
*
* 1. Test Structure: Use Given-When-Then pattern
* 2. Naming: Use descriptive test names (@DisplayName)
* 3. Single Responsibility: One assertion per test concept
* 4. Independence: Tests should not depend on each other
* 5. Fast: Tests should run quickly
* 6. Deterministic: Same input should always produce same output
* 7. Meaningful Assertions: Use descriptive assertion messages
* 8. Mock Wisely: Only mock external dependencies
* 9. Coverage: Test both happy path and edge cases
* 10. Organization: Use nested tests for complex scenarios
*/
public class TestingBestPractices {
/*
GOOD TEST CHARACTERISTICS:
- Fast
- Isolated
- Repeatable
- Self-validating
- Timely
- Readable
- Maintainable
*/
}
Summary
Key JUnit 5 Features:
- Annotations:
@Test,@BeforeEach,@AfterEach,@BeforeAll,@AfterAll - Assertions:
assertEquals(),assertTrue(),assertThrows(), etc. - Parameterized Tests:
@ParameterizedTestwith various sources - Mocking: Mockito integration with
@Mockand@InjectMocks - Advanced Testing: Nested tests, dynamic tests, repeated tests
Testing Pyramid:
- Unit Tests: Fast, isolated tests of individual components
- Integration Tests: Test interactions between components
- End-to-End Tests: Test complete system workflows
Tools and Libraries:
- JUnit 5: Core testing framework
- AssertJ: Fluent assertions
- Mockito: Mocking framework
- Spring Boot Test: Integration testing support
Effective testing leads to more reliable, maintainable, and bug-free code. Always aim for good test coverage and follow testing best practices.