Java Testing with JUnit

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: @ParameterizedTest with various sources
  • Mocking: Mockito integration with @Mock and @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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper