JUnit

assertEquals

In JUnit, assertEquals is a method used to check if the expected value and the actual value are equal in unit tests.

SHOW CODE
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class UnitTest {
    @Test
    void addition() {
        Calculator calculator = new Calculator();
        int result = calculator.add(1, 1);
        assertEquals(2, result, "The result of 1 + 1 should be 2");
    }
}

assertTure & assertFalse

In JUnit, the assertTrue and the assertFalse methods are used to validate boolean expressions in unit tests.

SHOW CODE
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class UnitTest {
    @Test
    void testIsOdd() {
        int n = 5;
        assertTrue(n % 2 == 1, "Number should be an odd");
        assertFalse(n % 2 == 0, "Number should not be an even");
    }
}

@BeforeEach & @AfterEach

In JUnit, the @BeforeEach and @AfterEach annotations are used to specify methods that should run before or after each test method, respectively. These annotations are useful for setting up resources before each test and cleaning up resources after each test.

SHOW CODE
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

public class UnitTest {
    private Calculator calculator;

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

    @Test
    void testAddition() {
        assertEquals(2, calculator.add(1, 1),
                "The result of 1 + 1 should be 2.");
    }

    @AfterEach
    void teardown() {
        calculator = null;
    }

}

@BeforeAll & @AfterAll

In JUnit, the @BeforeAll and @AfterAll annotations are used to specify methods that run once before and after all the test methods in a test class, respectively. These methods are commonly used for expensive setup and cleanup.

SHOW CODE
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class UnitTest {
    @BeforeAll
    public static void setUpBeforeClass() {
        System.out.println("Executed before all tests are run.");
    }

    @AfterAll
    public static void tearDownAfterClass() {
        System.out.println("Executed after all tests are run.");
    }

    @Test
    public void testMethod1() {
        System.out.println("Test 1 executed.");
    }

    @Test
    public void testMethod2() {
        System.out.println("Test 2 executed.");
    }
}
SHOW OUTPUT
Executed before all tests are run.
Test 1 executed.
Test 2 executed.
Executed after all tests are run.

assertArrayEquals & assertNotEquals

In JUnit, the assertArrayEquals and assertNotEquals methods are used to check if both arrays are equal in sizeand whether each corresponding element is equal or not. For arrays containing objects, JUnit will use the equals method of the objects for the comparison.

SHOW CODE
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

public class UnitTest {
    @Test
    void testArrayEquality() {
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 3};
        assertArrayEquals(expected, actual, "Arrays should be equal");

        String[] expected2 = {"apple", "banana", "cherry"};
        String[] actual2 = {"apple", "banana", "cherry"};

        // Compare arrays of objects (Strings in this case)
        assertArrayEquals(expected2, actual2);
    }

    @Test
    void testArrayNotEquality() {
        int[] expected = {1, 2, 3};
        int[] actual = {1, 2, 4};
        assertNotEquals(expected, actual, "Arrays should not be equal");
    }
}

assertThrows

In Junit, the assertThrows is a method used to assert that a specific exception is throwing during the execution of a piece of code.

SHOW CODE
import org.junit.jupiter.api.Test;

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

public class UnitTest {
    @Test
    void testDivision() {
        Calculator calculator = new Calculator();

        ArithmeticException exception = assertThrows(ArithmeticException.class, () -> {
            calculator.divide(1, 0);
        });

        assertEquals("/ by zero", exception.getMessage());
    }
}

assertTimeout & assertTimeoutPreemptively

In JUnit, the assertTimeout and assertTimeoutPreemptively methods are used to ensure that a unit test finishes within a specified time limit. The key difference between them is that the assertTimeoutPreemptively method will terminate the test as soon as the excution time exceeds the specified limit. In contrast, the assertTimeout method allows the test to complete before checking if the time limit was exceeded.

SHOW CODE
import org.junit.jupiter.api.Test;

import java.time.Duration;

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

public class UnitTest {
    @Test
    void testExecutionWithinTimeoutPreemptively() {
        assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
            // Simulate a task that takes more time than allowed
            Thread.sleep(1500); // Task that takes more than 1000ms
        });
    }

    @Test
    void testExecutionWithinTimeout() {
        assertTimeout(Duration.ofMillis(1000), () -> {
            // Simulate a task that takes some time
            Thread.sleep(500); // Task that takes less than 1000ms
        });
    }
}

Parameterized Tests

In JUnit, Parameterized Tests allow a test to be run with different parameters and expected results, which reduces repetitive code and increases test coverage. Common parameter sources include @ValueSource, which passes a group of values to the test; @CsvSource, which allows passing multiple values separated by commmas; @MethodSource, which generates values dynamically from a method; and @EnumSource, which passes enum constants from an enum class.

SHOW CODE
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.stream.Stream;

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


public class UnitTest {
    @ParameterizedTest
    @ValueSource(strings = {"apple", "banana", "cherry"})
    void testWithStringParameter(String fruit) {
        assertTrue(fruit.length() > 3);
    }

    @ParameterizedTest
    @CsvSource({
            "apple, 5",
            "banana, 6",
            "cherry, 6"
    })
    void testWithCsvSource(String fruit, int length) {
        assertEquals(fruit.length(), length);
    }

    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana", "cherry");
    }

    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithMethodSource(String fruit) {
        assertTrue(fruit.length() > 3);
    }

    enum FRUIT {APPLE, BANANA, CHERRY}

    @ParameterizedTest
    @EnumSource(FRUIT.class)
    void testWithEnumSource(FRUIT fruit) {
        assertTrue(fruit.name().length() > 3);
    }
}

Mockito

System Under Test

The System Under Test (SUT) refers to the specific part of the software being tested in a given test case. It can range from a single method in unit testing to larger components in integration or system testing based on different context. The SUT is typically isolated using mocking and stubbing to test its behavior independently.

SHOW CODE
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

public class CalculatorTest {
    @Test
    void testAdd() {
        Calculator calculator = new Calculator();  // SUT
        int result = calculator.add(2, 3);  // The add method is the System Under Test
        assertEquals(5, result);
    }
}

Mocking vs. Stubbing

In unit testing, Mocking and Stubbing are used to simulate and control the behavior of external dependencies. Mocking is commonly used for behavior verification, where objects are created to simulate the behavior of real-world components in a controlled manner. On the other hand, Stubbing is typically used to isolate the SUT by controlling the return values or responses from dependencies.

SHOW CODE: Mocking
public class Calculator {
    private Logger logger;

    public Calculator(Logger logger) {
        this.logger = logger;
    }

    public int add(int a, int b) {
        int result = a + b;
        logger.log("Addition performed: " + result);
        return result;
    }
}

@Test
void testAdd() {
    Logger mockLogger = mock(Logger.class);
    Calculator calculator = new Calculator(mockLogger);
    
    calculator.add(2, 3);
    
    // Verifying the interaction with the mock: was log method called?
    verify(mockLogger).log("Addition performed: 5");
}
SHOW CODE: Stubbing
public class Calculator {
    private DatabaseService databaseService;

    public Calculator(DatabaseService databaseService) {
        this.databaseService = databaseService;
    }

    public int getProductPrice(int productId) {
        Product product = databaseService.getProduct(productId);
        return product.getPrice();
    }
}

@Test
void testGetProductPrice() {
    // Stub the DatabaseService to return a fixed product
    DatabaseService stubbedService = mock(DatabaseService.class);
    Product stubProduct = new Product(1, "Laptop", 1000);
    when(stubbedService.getProduct(1)).thenReturn(stubProduct);
    
    Calculator calculator = new Calculator(stubbedService);
    int price = calculator.getProductPrice(1);
    
    assertEquals(1000, price);
}

mock(), when().thenReturn(), verify()

In Mockito, the mock() method is used to create a mock object, which allows simulating the behavior of an object without invoking its real methods. The when() method is used to define the behavior of a mock object’s method when it is called with specific arguments. The thenReturn() method specifies the return value of a mocked method. The verify() method is used to check whether a specified method on a mock object was called during the test execution.

SHOW CODE
import org.junit.jupiter.api.Test;

import java.util.List;

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

public class UnitTest {
    @Test
    public void testMockingBehavior() {
        // Create a mock List object
        List<String> mockList = mock(List.class);

        // Specify behavior for when add() is called
        when(mockList.add("Hello")).thenReturn(true);  // Return true when "Hello" is added

        // Call the mock method
        boolean result = mockList.add("Hello");

        // Verify the behavior
        assertTrue(result);  // The add method should return true
        verify(mockList).add("Hello");  // Verify that add("Hello") was called

        // Verify that add("World") was never called
        verify(mockList, never()).add("World");
    }
}

Argument Matchers

In Mockito, argument matchers are used to specify conditions for method arguments when stubbing methods. They are commonly used in the when() and thenReturn() methods when the exact method argument is not critical. Some commonly used argument marchers include:

  • any(): Matches any argument of the specified type.
  • eq(): Matches a specified argument.
  • anyString(): Matches any argument of type String.
  • anyInt(): Matches any argument of type Integer.
  • isA(): Matches an argument of a specified class type.
  • argThat(): Allows for custom argument matching logic.
SHOW CODE
import org.junit.jupiter.api.Test;

import java.util.List;

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

public class UnitTest {
    @Test
    public void testAnyMatcher() {
        List mockList = mock(List.class);

        // Stub behavior using argument matcher
        when(mockList.get(anyInt())).thenReturn("Signal");

        // Verify that any argument passed to get will return "Signal"
        assertEquals("Signal", mockList.get(9));
        assertEquals("Signal", mockList.get(999));
    }

    @Test
    public void testEqMatcher() {
        Calculator calculator = mock(Calculator.class);

        when(calculator.add(eq(1), eq(1))).thenReturn(2);

        assertEquals(2, calculator.add(1, 1));
        assertNotEquals(3, calculator.add(1, 1));
    }

    @Test
    public void testAnyStringMatcher() {
        // Create a mock List
        List<String> mockList = mock(List.class);

        // Stub the method to return "Hello" for any string argument
        when(mockList.contains(anyString())).thenReturn(true);

        // Verify that the mock returns true for any string passed as an argument
        assertTrue(mockList.contains("Test"));    // Matches any string
        assertTrue(mockList.contains("Hello"));   // Matches any string
        assertTrue(mockList.contains("World"));   // Matches any string
    }

    @Test
    public void testAnyIntMatcher() {
        // Create a mock List
        List mockList = mock(List.class);

        // Stub the method to return "Found" for any integer argument
        when(mockList.get(anyInt())).thenReturn("Found");

        // Verify that the mock returns "Found" regardless of the argument
        assertEquals("Found", mockList.get(1));   // Matches any integer
        assertEquals("Found", mockList.get(999)); // Matches any integer
    }

    @Test
    public void testArgThatMatcher() {
        // Create a mock List
        List<String> mockList = mock(List.class);

        // Use argThat to match arguments that are not null and have length greater than 3
        when(mockList.add(argThat(argument ->
                argument != null && argument.length() > 3)))
                .thenReturn(true);

        // Verify that the mock behaves correctly
        assertTrue(mockList.add("Hello"));   // Matches: length > 3
        assertFalse(mockList.add(""));       // Does not match: empty string
    }
}

Essential Mockito Annotaitons

The key annotations in Mockito are @Mock, @InjectMocks, @ExtendWith, and @Captor. The @Mock annotation is used to create mock objects. The @InjectMocks annotation injects the objects, created with @Mock, into the specified class under test. The @ExtendWith annotation specifies a custom test runner to use when executing the test. The @Captor annotation is used to create an ArgumentCaptor instance, which captures and verifies the arguments passed to mocked methods.

SHOW CODE
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
public class UnitTest {
    @Mock
    private List<String> mockList; // Mocked dependency

    @InjectMocks
    private MyClass myClass;  // Class under test

    @Captor
    private ArgumentCaptor<String> captor; // Captures arguments passed to the mock

    @Test
    void testAddItem() {
        // Perform the action under test
        myClass.addItem("Test Item");

        // Verify that the mockList's add method was called with the argument "Test Item"
        verify(mockList).add(captor.capture());

        // Capture and assert the argument
        String capturedArgument = captor.getValue();
        System.out.println("capturedArgument = " + capturedArgument);

        // Assert the captured item is correct
        assertEquals("Test Item", capturedArgument);
    }

    static class MyClass {
        private final List<String> list;

        public MyClass(List<String> list) {
            this.list = list;
        }

        public void addItem(String item) {
            list.add(item);
        }
    }
}

Mockit Spy

Mockito’s spy is a partial mock, typically created using @Spy, where real methods are used by default, and only the specified methods are stubbed.

SHOW CODE
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
public class UnitTest {
    // Create a spy for the class under test
    @Spy
    private MyClass myClass = new MyClass();

    // Inject the mock dependency into the class under test
    @Mock
    private List<String> mockList;

    @Test
    public void testAddItemWithSpy() {
        // Use the real method to add an item
        myClass.addItem("Test Item");

        // Verify that the mock's addItem method is called with the argument
        // Fail the test: Actually, there were zero interactions with this mock.
        verify(mockList).add("Test Item");

        // Stub the method for testing
        doReturn("Mocked Response").when(myClass).getGreeting();

        // Now the method getGreeting() will return the mocked response
        String response = myClass.getGreeting();
        System.out.println(response);  // Output: Mocked Response

        // Assert the mocked behavior
        assertEquals("Mocked Response", response);
    }

    // Class under test
    static class MyClass {
        private final List<String> list = new ArrayList<>();

        public void addItem(String item) {
            list.add(item);
        }

        public String getGreeting() {
            return "Hello, World!";
        }
    }
}

In the above code, the statement verify(mockList).add("Test Item"); will fail because mockList is a partial mock. By default, real methods are called on a spy object, and since the real method is not stubbed or tracked. the verification cannot succeed.


Mockito and SpringBoot Example

SHOW CODE: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.4.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>dev.signalyu</groupId>
	<artifactId>springtest</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springtest</name>
	<description>springtest</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
		<dependency>
			<groupId>org.mockito</groupId>
			<artifactId>mockito-core</artifactId>
			<version>5.14.2</version>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<version>3.4.0</version>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.34</version>
			<scope>provided</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<version>3.4.0</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-commons -->
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-commons</artifactId>
			<version>3.4.0</version>
		</dependency>
	</dependencies>

<!--	<build>-->
<!--		<plugins>-->
<!--			<plugin>-->
<!--				<groupId>org.apache.maven.plugins</groupId>-->
<!--				<artifactId>maven-compiler-plugin</artifactId>-->
<!--				<configuration>-->
<!--					<annotationProcessorPaths>-->
<!--						<path>-->
<!--							<groupId>org.projectlombok</groupId>-->
<!--							<artifactId>lombok</artifactId>-->
<!--						</path>-->
<!--					</annotationProcessorPaths>-->
<!--				</configuration>-->
<!--			</plugin>-->
<!--			<plugin>-->
<!--				<groupId>org.springframework.boot</groupId>-->
<!--				<artifactId>spring-boot-maven-plugin</artifactId>-->
<!--				<configuration>-->
<!--					<excludes>-->
<!--						<exclude>-->
<!--							<groupId>org.projectlombok</groupId>-->
<!--							<artifactId>lombok</artifactId>-->
<!--						</exclude>-->
<!--					</excludes>-->
<!--				</configuration>-->
<!--			</plugin>-->
<!--		</plugins>-->
<!--	</build>-->

</project>
SHOW CODE
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Data
class User {
    private Long id;
    private String name;
}

@Repository
interface UserRepository extends CrudRepository<User, Long> {

}

@Service
@Data
class UserService {
    @Autowired
    private final UserRepository userRepository;

    public String getUserById(Long id) {
        User user = userRepository.findById(id).orElse(null);
        return user == null ? "User not found" : user.getName();
    }
}

@RestController
@AllArgsConstructor
class UserController {
    private final UserService userService;

    @GetMapping("/user/{id}")
    public String getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }
}

@SpringBootTest
@AutoConfigureMockMvc
public class UnitTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private UserService userService;

    @Test
    public void testGetUser() throws Exception {
        Long userId = 1L;
        String mockResponse = "Signal Yu";

        when(userService.getUserById(userId)).thenReturn(mockResponse);

        mockMvc.perform(get("/user/{id}", userId))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(mockResponse));
    }

}