# Review No. 9 (JUnit, Mockito, etc)

Published 2024-11-03

This page contains Java concepts briefly explained.

# JUnit 4 vs JUnit 5

  • JUnit 4 -> Java 5 +
  • JUnit 5 (modular approach) -> Java 8 +

# @BeforeEach vs @BeforeAll

  • @BeforeEach -> Runs multiple times — once before each test method.
  • @BeforeAll -> Runs ONE times — the method runs once per test class.

# Can we disable a test in JUnit 5 ?

Yes, using the @Disabled annotation. Optionally, provide a reason: @Disabled("Reason for disabling").

# What is @Nested in JUnit 5 ?

@Nested is an annotation in JUnit 5 that allows you to create nested test classes within a test class (used at subclass level).

# What is @ParameterizedTest annotation used for ?

@ParameterizedTest is an annotation in JUnit 5 that allows you to run the same test multiple times with different parameters. It’s particularly useful when you want to test a method with a variety of inputs without duplicating the test code.

# Why we need to mark a method with @Test ?

@Test is used to inform the JUnit framework that the annotated method is a test case that should be executed as part of the test suite. Without @Test: The method would be treated as a regular method and would not be executed
automatically during the test run
.

# How can we handle exceptions in JUnit 5 ?

  • Using assertThrows
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;

class ExampleTest {

    @Test
    void testException() {
        assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("Invalid argument");
        });
    }
}
  • validating the Exception Message
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ExampleTest {

    @Test
    void testExceptionMessage() {
        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("Invalid argument");
        });
        
        assertEquals("Invalid argument", exception.getMessage());
    }
}
  • using assertDoesNotThrow
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

class ExampleTest {

    @Test
    void testNoException() {
        assertDoesNotThrow(() -> {
            // Code that should not throw any exceptions
            int result = 10 / 2;
        });
    }
}

# MockitoExtension vs SpringExtension

MockitoExtension SpringExtension
No context loading Loads Spring application context
Unit testing with mocks Integration testing with Spring context
Faster Slower
Mocks and spies (@Mock, @InjectMocks) Spring-managed beans (@Autowired)

# @EnabledIf or @DisabledIf

Used for writing test cases for methods that should run only if certain conditions are met.

# Why use @Tag annotation ?

The @Tag annotation in JUnit 5 allows you to categorize and organize test methods.

Example:

@Test
@Tag("integration")
@Tag("database")
void testDatabaseIntegration() {
    // Test logic involving database integration
}

You can specify tags when running tests via Maven, Gradle, or other build tools.

mvn test -Dgroups="fast"
./gradlew test --tests * --tags fast

# @TestInstance

By default, JUnit creates a new test instance for each test method (PER_METHOD lifecycle). However, with @TestInstance, you can change this behavior to use a single instance for all test methods (PER_CLASS lifecycle).

Example:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

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

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PerClassLifecycleTest {

    private int counter = 0;

    @Test
    void testIncrement1() {
        counter++;
        assertEquals(1, counter); // First test, counter is 1
    }

    @Test
    void testIncrement2() {
        counter++;
        assertEquals(2, counter); // Second test, counter is 2
    }
}

# @DisplayName

@DisplayName allows you to provide a custom, human-readable name for your test methods, improving readability and making test reports more understandable.

# How can you ensure tests are run in a specific order ?

JUnit 5 does not guarantee the order of test execution by default.

@TestMethodOrder annotation can be used to specify the order of the tests.

Example:

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static org.junit.jupiter.api.Assertions.assertTrue;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {

    @Test
    @Order(1)
    void testA() {
        System.out.println("Running testA");
        assertTrue(true);
    }

    @Test
    @Order(2)
    void testB() {
        System.out.println("Running testB");
        assertTrue(true);
    }
}

# What is the difference between assertAll and regular assertions ?

Example:

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

assertAll("group of assertions",
    () -> assertEquals(2, 1 + 1, "Addition check"),
    () -> assertTrue(5 > 2, "Greater than check"),
    () -> assertEquals("hello", "hello", "String equality")
);

Using assertAll we can see the result of all tests in the group.

# How do you handle timeout in JUnit 5 ?

assertTimeout(Duration.ofMillis(100), () -> {
// Code that should complete within 100 milliseconds
});

assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
// Code that should complete within 1 second
});
  • assertTimeout : If the timeout is exceeded, the method waits for the code to complete before failing the test.
  • assertTimeoutPreemptively: If the timeout is exceeded, the method stops and the test is not passed.

# What is MockMvc ?

MockMvc is a utility provided by Spring Framework for testing Spring MVC web applications, including controllers & Spring Security.

To use MockMvc, you typically annotate the test class with:

  • @WebMvcTest
  • @SpringBootTest and @AutoConfigureMockMvc

# Examples using MockMvc

Controller with Path Variable

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void whenGetUserWithId_thenReturnsUserId() throws Exception {
        mockMvc.perform(get("/user/1"))
               .andExpect(status().isOk())
               .andExpect(content().string("User ID: 1"));
    }
}

Controller Handling JSON Requests and Responses

@WebMvcTest(ApiController.class)
public class ApiControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void whenPostData_thenReturnsProcessedData() throws Exception {
        mockMvc.perform(post("/api/data")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"key\":\"value\"}"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.response").value("Received: value"));
    }
}

Controller with Spring Security

@WebMvcTest(SecureController.class)
public class SecureControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void whenUnauthenticated_thenRedirectToLogin() throws Exception {
        mockMvc.perform(get("/secure"))
               .andExpect(status().is3xxRedirection())
               .andExpect(redirectedUrlPattern("**/login"));
    }

    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    public void whenAuthenticated_thenAccessGranted() throws Exception {
        mockMvc.perform(get("/secure"))
               .andExpect(status().isOk())
               .andExpect(content().string("Secure Content"));
    }
}

Controller with Exception Handling

@WebMvcTest(ExceptionController.class)
public class ExceptionControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void whenExceptionThrown_thenHandled() throws Exception {
        mockMvc.perform(get("/exception"))
               .andExpect(status().isInternalServerError())
               .andExpect(content().string("Handled Exception: Exception occurred"));
    }
}

Controller with Dependency Injection

@WebMvcTest(ServiceController.class)
public class ServiceControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyService myService;

    @Test
    public void whenServiceCalled_thenReturnsMockedData() throws Exception {
        // Given
        when(myService.getData()).thenReturn("Mocked Service Data");

        // When 
        mockMvc.perform(get("/service"))
                //Then
               .andExpect(status().isOk())
               .andExpect(content().string("Mocked Service Data"));
    }
}

# How could I test a @Repository bean in Spring ?

  1. Using @DataJpaTest for JPA Repositories
@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void whenSaveUser_thenFindById() {
        // Given
        User user = new User();
        user.setName("John Doe");
        userRepository.save(user);

        // When
        Optional<User> foundUser = userRepository.findById(user.getId());

        // Then
        assertTrue(foundUser.isPresent());
        assertEquals("John Doe", foundUser.get().getName());
    }
}
  1. Using @SpringBootTest

We can use @SpringBootTest as in the example above. In this case, the entire application context is loaded, including all the beans, and can be used to test repository beans with a real or in-memory database.

  1. Using Testcontainers -> Great for realistic integration tests with real databases running in Docker containers.

# Unit & Integration tests with Maven

Unit tests

  • to run : mvn test (the "test" phase)
  • run by Surefire Plugin
  • run from src/test/java

Integration tests

  • to run : mvn verify (the "verify" phase)
  • run by Failsafe Plugin
  • run from src/integration-test/java or src/test/java with a name using the Failsafe convention (*IT.java).

# @Mock vs @InjectMocks

@Mock
// Mock dependency
private ShippingService shippingService; 

@InjectMocks
// Class under test with dependencies injected
private OrderService orderService;