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.