Three points to master Spring Boot unit testing

Unit testing is an indispensable and important link in software development. It is used to verify the accuracy of the smallest testable unit in the software. Combining Spring Boot, JUnit, Mockito and layered architecture, developers can more easily write reliable, testable and high-quality unit test code to ensure the correctness and quality of software.

1. Introduction

This article will start with technical topics related to unit testing. After the technical part, it will introduce the practice of unit testing using Spring Boot, JUnit and Mockito.

2. Key elements of testing

1 unit

The word unit in unit testing refers to the smallest functional part of the software that can be tested and processed independently, usually referring to independent code fragments such as functions, methods, classes, or modules.

2. Use cases

Use cases describe the ways in which a system uses specific functions or features and are used to understand, design, and test the requirements of a software system. Typically includes details such as how users interact with the system, their expectations of the system, and the results it should achieve.

3. Boundary situations

Edge cases refer to specific scenarios that software must handle that include unexpected or boundary conditions that differ from typical situations or are considered rare. Edge cases can include unexpected user logins, testing constraints, unusual input, or other situations that may cause system errors or unusual behavior. During testing, it is very important to consider and test edge cases as they can help developers discover potential problems and ensure the robustness and stability of the system.

3. Unit testing

Unit tests cover every possibility we can think of and write. Each unit must have at least one test method. Tests are not written for a method, but for a unit.

Unit tests can be written in the following order: normal path/use case, edge cases, and exception cases.

These steps are essential to ensure that the unit processes the input in the correct way and produces the expected output, exhibiting the expected behavior. Unit testing is the best way to detect risks and fix bugs early. Through unit testing, we can prevent potential unexpected situations, respond to changes in production code, and ensure that the production code can handle various situations. In short, unit testing ensures the safety of production code.

Another important thing about unit testing is to test the business logic, instead of testing the infrastructure code in unit tests, the infrastructure code can be tested in integration tests. Consider using some architectural patterns (such as onion architecture, hexagonal architecture, etc.) to separate business logic from infrastructure code.

Another advantage of unit testing is that it is fast because it does not need to rely on the Spring ApplicationContext. Due to context, integration tests in the same test pyramid are much slower compared to unit tests.

1. Start coding

In a layered architecture project, business code is mainly located in the service layer. This means that the service layer has units and needs to be tested. Let's focus on the most critical part.

Here is a sample code:

  @Override
    public String saveUser(User user) {
        validateUser(user);
        try {
            User savedUser = userRepository.save(user);
            return savedUser.getEmail();
        } catch (Exception exception) {
            throw new IllegalArgumentException(E_GENERAL_SYSTEM);
        }
    }

    private void validateUser(User user) {
        if (Objects.isNull(user.getEmail())) {
            throw new IllegalArgumentException(E_USER_EMAIL_MUST_NOT_BE_NULL);
        }
        if (findByEmail(user.getEmail()).isPresent()) {
            throw new IllegalArgumentException(E_USER_ALREADY_REGISTERED);
        }
    }

    @Override
    public Optional<User> findByEmail(String email) {
        return userRepository.findByEmail(email);
    }

There are two public methods and one private method in the above code. The private method can be regarded as part of the public method. In addition, due to the complexity and functional requirements of the code, there are many possible scenarios that require writing multiple test cases to cover various situations to ensure the correctness of the code.

2. Annotation

@ExtendWith is used to integrate the Mockito library into JUnit tests. @Test marks a method as a test method that contains specified test cases and is automatically run by JUnit.

During testing, you need to mock the dependencies of the class being tested. The reason mentioned before is that since the Spring ApplicationContext won't start, we can't inject dependencies into the context. @Mock is used to create a mock dependency, and @InjectMocks is used to inject these mocked dependencies into the class under test.

@BeforeEach and @AfterEach can be used to perform corresponding actions before and after each method runs.

@ParameterizedTest is used to run repeated test cases with different parameter values. By using @ValueSource, you can provide different parameter values ​​to a method for multiple tests.

3. Three Main Stages of Testing Methodology

  • Given: Objects required to prepare test cases
  • When: Perform necessary actions to run the test scenario
  • Then: Check or verify expected results

doReturn/when is used to determine how a method behaves when given the specified arguments. However, since the dependency is @Mock, it won't actually be executed.

verify is used to check whether the code under test behaves as expected. If the method to be tested is of public void type, you can use verify for verification.

Assertions are used to verify expected results.

 @ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UserRepository userRepository;

    private User user;
    public static final String MOCK_EMAIL = "[email protected]";

    @BeforeEach
    void setUp() {
        user = new User();
        System.out.println("init");
    }

    @AfterEach
    void teardown() {
        System.out.println("teardown");
    }

    @ParameterizedTest
    @ValueSource(strings = {"[email protected]", "[email protected]"})
    @DisplayName("Happy Path: save user use cases")
    void givenCorrectUser_whenSaveUser_thenReturnUserEmail(String email) {
        // given
        user.setUserName("mertbahardogan").setEmail(email).setPassword("pass");
        User savedUser = new User().setEmail(email);
        doReturn(savedUser).when(userRepository).save(any());

        // when
        String savedUserEmail = userService.saveUser(user);

        // then
        verify(userRepository,times(1)).findByEmail(anyString());
        verify(userRepository,times(1)).save(any());
        assertEquals(email, savedUserEmail);
    }

    @Test
    @DisplayName("Exception Test: user email must not be null case")
    void givenNullUserEmail_whenSaveUser_thenThrowsEmailMustNotNullEx() {
        // when
        Exception exception = assertThrows(IllegalArgumentException.class, () -> userService.saveUser(user));

        // then
        assertNotNull(exception);
        assertEquals(E_USER_EMAIL_MUST_NOT_BE_NULL, exception.getMessage());
    }

    @Test
    @DisplayName("Exception Test: user is already registered case")
    void givenRegisteredUser_whenSaveUser_thenThrowsUserAlreadyRegisteredEx() {
        // given
        user.setEmail(MOCK_EMAIL);
        Optional<User> savedUser = Optional.of(new User().setEmail(MOCK_EMAIL));
        doReturn(savedUser).when(userRepository).findByEmail(anyString());

        // when
        Exception exception = assertThrows(IllegalArgumentException.class, () -> userService.saveUser(user));

        // then
        assertNotNull(exception);
        assertEquals(E_USER_ALREADY_REGISTERED, exception.getMessage());
    }

    @Test
    @DisplayName("Exception Test: catch case")
    void givenIncorrectDependencies_whenSaveUser_thenThrowsGeneralSystemEx() {
        // given
        user.setEmail(MOCK_EMAIL);

        // when
        Exception exception = assertThrows(IllegalArgumentException.class, () -> userService.saveUser(user));

        // then
        assertNotNull(exception);
        assertEquals(E_GENERAL_SYSTEM, exception.getMessage());
    }

    @Test
    @DisplayName("Happy Path: find user by email")
    void givenCorrectUser_whenFindByEmail_thenReturnUserEmail() {
        // given
        Optional<User> savedUser = Optional.of(new User().setEmail(MOCK_EMAIL));
        doReturn(savedUser).when(userRepository).findByEmail(anyString());

        // when
        Optional<User> user = userService.findByEmail(MOCK_EMAIL);

        // then
        verify(userRepository,times(1)).findByEmail(anyString());
        assertEquals(savedUser, user);
    }
}

The UserServiceImpl test class runs for 1 second and 693 milliseconds.

Introducing a software development tool

Successful front-end engineers are good at using tools. In recent years, the low-code concept has become popular, such as Mendix abroad and JNPF domestically. This new development method has a graphical drag-and-drop configuration interface and is compatible with custom components. Code expansion has indeed greatly improved efficiency in the construction of B-side backend management websites.

JNPF development platform, many people have used it, it is a master of functions, and any information system can be developed based on it.

The principle is to concretize certain recurring scenes and processes in the development process into components, APIs, and database interfaces to avoid reinventing the wheel. This greatly improves programmers' productivity.

Official website: http://www.jnpfsoft.com/?csdn . If you have free time, you can expand your knowledge.

This is a simple, cross-platform rapid development framework based on Java Boot/.Net Core. The front-end and back-end encapsulate thousands of common classes for easy expansion; it integrates a code generator to support front-end and front-end business code generation to meet rapid development and improve work efficiency; the framework integrates various commonly used classes such as forms, reports, charts, and large screens. Demo is easy to use directly; the back-end framework supports Vue2 and Vue3.

In order to support application development with higher technical requirements, from database modeling, Web API construction to page design, there is almost no difference from traditional software development. However, the low-code visualization mode reduces the repetitive labor of building "add, delete, modify and check" functions.

Guess you like

Origin blog.csdn.net/wangonik_l/article/details/133383214
Recommended