Skip to content

Java Testing Frameworks

This guide covers Java testing frameworks, best practices, and strategies for writing effective tests.

🎯 Testing Fundamentals

Testing Pyramid

  • Unit Tests: Fast, isolated tests for individual components
  • Integration Tests: Test component interactions
  • End-to-End Tests: Test complete user workflows
  • Performance Tests: Load and stress testing

Testing Principles

  • AAA Pattern: Arrange, Act, Assert
  • First Principles: Fast, Independent, Repeatable, Self-validating, Timely
  • Test Coverage: Aim for high coverage of critical paths
  • Test Naming: Descriptive test names that explain behavior

🧪 JUnit Framework

JUnit 5 (Jupiter)

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    @DisplayName("Should add two positive numbers")
    void shouldAddTwoPositiveNumbers() {
        // Arrange
        int a = 5;
        int b = 3;

        // Act
        int result = calculator.add(a, b);

        // Assert
        assertEquals(8, result, "5 + 3 should equal 8");
    }

    @Test
    @DisplayName("Should throw exception for division by zero")
    void shouldThrowExceptionForDivisionByZero() {
        // Arrange
        int a = 10;
        int b = 0;

        // Act & Assert
        assertThrows(ArithmeticException.class, () -> calculator.divide(a, b));
    }

    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 4, 5})
    @DisplayName("Should handle positive numbers")
    void shouldHandlePositiveNumbers(int number) {
        assertTrue(calculator.isPositive(number));
    }

    @Test
    @Disabled("Feature not yet implemented")
    @DisplayName("Should calculate square root (disabled)")
    void shouldCalculateSquareRoot() {
        // Test for future feature
    }
}

JUnit 5 Advanced Features

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class AdvancedCalculatorTest {

    @TestFactory
    Stream<DynamicTest> dynamicTestsForPalindrome() {
        PalindromeChecker checker = new PalindromeChecker();

        return Stream.of("racecar", "level", "radar")
            .map(word -> DynamicTest.dynamicTest(
                "Test palindrome: " + word,
                () -> assertTrue(checker.isPalindrome(word))
            ));
    }

    @ParameterizedTest
    @CsvSource({
        "2, 3, 6",
        "0, 5, 0",
        "-2, 3, -6"
    })
    @DisplayName("Should multiply numbers")
    void shouldMultiplyNumbers(int a, int b, int expected) {
        Calculator calculator = new Calculator();
        assertEquals(expected, calculator.multiply(a, b));
    }

    @RepeatedTest(5)
    @DisplayName("Should handle random operations")
    void shouldHandleRandomOperations(RepetitionInfo repetitionInfo) {
        Calculator calculator = new Calculator();
        int random1 = (int) (Math.random() * 100);
        int random2 = (int) (Math.random() * 100);

        int result = calculator.add(random1, random2);
        assertEquals(random1 + random2, result);
    }
}

🎭 Mockito Framework

Mockito Basics

import org.junit.jupiter.api.Test;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldCreateUserAndSendEmail() {
        // Arrange
        User user = new User("[email protected]", "password");
        when(userRepository.save(any(User.class))).thenReturn(user);
        doNothing().when(emailService).sendWelcomeEmail(user.getEmail());

        // Act
        User createdUser = userService.createUser(user.getEmail(), user.getPassword());

        // Assert
        assertNotNull(createdUser);
        assertEquals("[email protected]", createdUser.getEmail());

        // Verify interactions
        verify(userRepository).save(user);
        verify(emailService).sendWelcomeEmail(user.getEmail());
    }

    @Test
    void shouldThrowExceptionWhenEmailAlreadyExists() {
        // Arrange
        String email = "[email protected]";
        when(userRepository.existsByEmail(email)).thenReturn(true);

        // Act & Assert
        assertThrows(UserAlreadyExistsException.class, 
            () -> userService.createUser(email, "password"));

        // Verify no save or email sent
        verify(userRepository, never()).save(any(User.class));
        verify(emailService, never()).sendWelcomeEmail(anyString());
    }
}

Mockito Advanced

import org.junit.jupiter.api.Test;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
import static org.junit.jupiter.api.Assertions.*;

class AdvancedUserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Captor
    private ArgumentCaptor<User> userCaptor;

    @Test
    void shouldCreateUserWithCorrectData() {
        // Arrange
        UserService userService = new UserService(userRepository);
        String email = "[email protected]";
        String password = "password123";

        // Act
        userService.createUser(email, password);

        // Assert
        verify(userRepository).save(userCaptor.capture());
        User savedUser = userCaptor.getValue();

        assertEquals(email, savedUser.getEmail());
        assertNotNull(savedUser.getPasswordHash());
        assertNotEquals(password, savedUser.getPasswordHash()); // Should be hashed
    }

    @Test
    void shouldHandleRepositoryException() {
        // Arrange
        when(userRepository.save(any(User.class)))
            .thenThrow(new DatabaseException("Connection failed"));

        UserService userService = new UserService(userRepository);

        // Act & Assert
        assertThrows(UserCreationException.class, 
            () -> userService.createUser("[email protected]", "password"));
    }
}

🌐 Spring Testing

Spring Boot Testing

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureTestDatabase
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private EmailService emailService;

    @Test
    void shouldCreateUser() throws Exception {
        String userJson = "{\"email\":\"[email protected]\",\"password\":\"password\"}";

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(userJson))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.email").value("[email protected]"));

        verify(emailService).sendWelcomeEmail("[email protected]");
    }
}

Data JPA Testing

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import static org.junit.jupiter.api.Assertions.*;

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldFindUserByEmail() {
        // Arrange
        User user = new User("[email protected]", "hashedPassword");
        entityManager.persistAndFlush(user);

        // Act
        Optional<User> found = userRepository.findByEmail("[email protected]");

        // Assert
        assertTrue(found.isPresent());
        assertEquals("[email protected]", found.get().getEmail());
    }

    @Test
    void shouldReturnEmptyWhenUserNotFound() {
        // Act
        Optional<User> found = userRepository.findByEmail("[email protected]");

        // Assert
        assertFalse(found.isPresent());
    }
}

🚀 Testcontainers

Database Testing with Testcontainers

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestDatabase;

@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:13")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldCreateAndFindUser() {
        // Arrange
        User user = new User("[email protected]", "hashedPassword");

        // Act
        User saved = userRepository.save(user);
        Optional<User> found = userRepository.findByEmail("[email protected]");

        // Assert
        assertNotNull(saved.getId());
        assertTrue(found.isPresent());
        assertEquals("[email protected]", found.get().getEmail());
    }
}

📊 Performance Testing

JMH for Microbenchmarks

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class StringConcatenationBenchmark {

    private String[] strings;

    @Setup
    public void setUp() {
        strings = new String[1000];
        for (int i = 0; i < strings.length; i++) {
            strings[i] = "string" + i;
        }
    }

    @Benchmark
    public String concatenateWithPlus() {
        String result = "";
        for (String s : strings) {
            result += s;
        }
        return result;
    }

    @Benchmark
    public String concatenateWithStringBuilder() {
        StringBuilder sb = new StringBuilder();
        for (String s : strings) {
            sb.append(s);
        }
        return sb.toString();
    }

    @Benchmark
    public String concatenateWithStringJoin() {
        return String.join("", strings);
    }
}

🛠️ Testing Best Practices

Test Structure

class WellStructuredTest {

    // 1. Clear test name
    @Test
    @DisplayName("Should calculate discount for premium customer")
    void shouldCalculateDiscountForPremiumCustomer() {

        // 2. Arrange - set up test data
        Customer customer = new Customer();
        customer.setPremium(true);
        Order order = new Order(100.0);

        // 3. Act - execute the method under test
        DiscountCalculator calculator = new DiscountCalculator();
        double discount = calculator.calculateDiscount(customer, order);

        // 4. Assert - verify the result
        assertEquals(20.0, discount, "Premium customer should get 20% discount");
    }
}

Test Data Management

// Use builders for test data
public class UserBuilder {
    private String email = "[email protected]";
    private String password = "password";
    private boolean active = true;

    public static UserBuilder aUser() {
        return new UserBuilder();
    }

    public UserBuilder withEmail(String email) {
        this.email = email;
        return this;
    }

    public UserBuilder withPassword(String password) {
        this.password = password;
        return this;
    }

    public User build() {
        return new User(email, password, active);
    }
}

// Usage in tests
@Test
void shouldCreateActiveUser() {
    User user = UserBuilder.aUser()
        .withEmail("[email protected]")
        .build();

    assertTrue(user.isActive());
}

🔗 Language-Specific Testing