0


Spring Boot 中应用单元测试(UT):结合 Mock 和 H2 讲解和案例示范

1. 引言

单元测试的目标是验证程序中每个模块的正确性。通过编写单元测试,开发者可以确保功能按预期工作,并在未来的开发中减少引入缺陷的风险。Spring Boot 提供了强大的测试支持,结合 Mock 和 H2 数据库,可以高效地进行测试。

2. Spring Boot 单元测试基础

2.1 什么是单元测试?

单元测试是对程序中最小可测试单元(如方法或类)进行验证的过程。它通常由开发者编写,并使用测试框架(如 JUnit、Mockito)来执行。

2.2 Spring Boot 测试支持

Spring Boot 提供了

spring-boot-starter-test

依赖,该依赖包含了测试所需的常用库,如 JUnit、Mockito 和 AssertJ。可以通过以下 Maven 依赖引入:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3. 使用 H2 数据库进行测试

H2 数据库是一个轻量级的 Java SQL 数据库,广泛用于单元测试中,尤其是在 Spring Boot 应用中。由于 H2 是内存数据库,它非常适合快速的集成测试和单元测试,因为可以在每次测试前清空数据库,从而确保测试环境的一致性。

3.1 H2 数据库的配置

在 Spring Boot 项目中,配置 H2 数据库通常非常简单。只需在

application.properties

application.yml

文件中添加以下配置:

# application.properties
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

3.2 H2 控制台访问

H2 提供了一个网页控制台,方便开发人员在测试期间查看数据库内容。启用控制台后,访问

http://localhost:8080/h2-console

,输入 JDBC URL(如

jdbc:h2:mem:testdb

)以及相应的用户名和密码,即可登录。

3.3 在测试中使用 H2 数据库

在编写测试时,可以使用

@DataJpaTest

注解来简化设置,这样 Spring Boot 会自动配置 H2 数据库并扫描 JPA 相关组件。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jpa.DataJpaTest;
import org.springframework.test.annotation.Rollback;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
public class UserActivityRepositoryTest {

    @Autowired
    private UserActivityRepository userActivityRepository;

    @Test
    @Rollback(false) // 可选,避免测试后清空数据
    public void testSaveActivity() {
        UserActivity activity = new UserActivity();
        activity.setUserId("user1");
        activity.setAction("login");

        UserActivity savedActivity = userActivityRepository.save(activity);
        assertThat(savedActivity.getId()).isNotNull();
        assertThat(savedActivity.getUserId()).isEqualTo("user1");
    }
}

3.4 处理 MySQL 函数不兼容的场景

尽管 H2 数据库功能强大,但它并不完全兼容 MySQL 的所有特性。在测试中,如果你使用 MySQL 特有的函数或语法,可能会遇到问题。以下是一些常见的兼容性问题及其解决方案。

3.4.1 使用 H2 特性替代 MySQL 函数

对于 MySQL 中常见的函数,可以查阅 H2 文档,寻找相应的替代函数。例如:

  • **MySQL 的 NOW()**:在 H2 中可以使用 CURRENT_TIMESTAMP
  • **MySQL 的 IFNULL(col1, col2)**:在 H2 中使用 COALESCE(col1, col2)
3.4.2 通过
MODE

配置 MySQL 兼容性

H2 提供了一种方式来设置数据库模式,使其更接近 MySQL 的行为。可以通过以下配置设置 MySQL 模式:

spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE

这种模式使 H2 在处理 SQL 语句时遵循 MySQL 的某些行为。例如,它会识别 MySQL 的

AUTO_INCREMENT

关键字。

3.4.3 自定义 SQL 脚本

如果你的应用依赖于特定的 MySQL 函数或语法,可以通过编写 SQL 脚本,在 H2 数据库中创建所需的视图或存储过程。

CREATE ALIAS IF NOT EXISTS `IFNULL` AS $$
public static String ifnull(String str1, String str2) {
    return str1 != null ? str1 : str2;
}
$$;

4. Mock 对象的使用

Mock 对象在单元测试中扮演着至关重要的角色。它们用于模拟实际对象的行为,以便我们可以专注于测试特定模块,而无需担心其依赖的外部组件。

4.1 什么是 Mock?

Mock 是对真实对象的模拟,允许开发者定义预期的行为和返回值。通过使用 Mock,开发者可以隔离被测试的单元,从而提高测试的效率和可靠性。

4.2 使用 Mockito 创建 Mock

Mockito 是一个流行的 Java Mock 框架,提供了简单易用的 API。以下是创建和使用 Mock 对象的基本步骤:

  1. 添加依赖:确保在 Maven 中引入 Mockito 依赖(通常已包含在 spring-boot-starter-test 中)。
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.11.2</version>
    <scope>test</scope>
</dependency>
  1. 创建 Mock 对象:使用 @Mock 注解创建 Mock 对象,并使用 @InjectMocks 注解将其注入到被测试的类中。
@Mock
private UserActivityRepository userActivityRepository;

@InjectMocks
private UserActivityService userActivityService;
  1. 初始化 Mock 对象:在测试的 @BeforeEach 方法中使用 MockitoAnnotations.openMocks(this) 来初始化 Mock 对象。
  2. 定义 Mock 行为:使用 when(...).thenReturn(...) 来定义 Mock 对象的行为。例如:
when(userActivityRepository.save(any(UserActivity.class))).thenReturn(activity);
  1. 验证交互:使用 verify(...) 方法来验证 Mock 对象的交互,确保被测试的单元以正确的方式调用了依赖。
verify(userActivityRepository, times(1)).save(activity);

4.3 Mock 对象的优点

  • 解耦:使用 Mock 可以将被测试单元与外部依赖解耦,提高测试的独立性。
  • 可控性:可以控制 Mock 的行为和返回值,以测试不同的场景和边界条件。
  • 简化测试:通过 Mock,可以避免复杂的环境设置和状态管理,简化测试过程。

4.4 示例:使用 Mockito 进行 Mock 测试

下面是一个使用 Mockito 进行 Mock 测试的简单示例:

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

public class UserServiceTest {
    @Mock
    private UserActivityRepository userActivityRepository;

    @InjectMocks
    private UserActivityService userActivityService;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testSaveActivity() {
        UserActivity activity = new UserActivity();
        activity.setId("1");
        activity.setUserId("user1");
        activity.setAction("login");

        when(userActivityRepository.save(activity)).thenReturn(activity);

        UserActivity savedActivity = userActivityService.saveActivity(activity);
        assertEquals("user1", savedActivity.getUserId());
        verify(userActivityRepository, times(1)).save(activity);
    }
}

5. 数据分析系统案例

在本节中,我们将以一个简单的数据分析系统为案例,展示如何进行单元测试。

5.1 系统需求分析

数据分析系统需要处理用户行为数据,主要功能包括:

  • 存储用户行为记录。
  • 查询用户行为记录。
  • 分析用户活跃度。

5.2 数据模型设计

我们定义一个

UserActivity

类,表示用户行为数据:

import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
public class UserActivity {
    @Id
    private String id;
    private String userId;
    private String action;
    private LocalDateTime timestamp;

    // Getters and Setters
}

5.3 Repository 接口

定义一个

UserActivityRepository

接口用于数据访问:

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserActivityRepository extends JpaRepository<UserActivity, String> {
    List<UserActivity> findByUserId(String userId);
}

5.4 服务层实现

在服务层中实现用户行为数据的存储和查询逻辑:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserActivityService {

    @Autowired
    private UserActivityRepository userActivityRepository;

    public UserActivity saveActivity(UserActivity activity) {
        return userActivityRepository.save(activity);
    }

    public List<UserActivity> getActivitiesByUserId(String userId) {
        return userActivityRepository.findByUserId(userId);
    }
}

5.5 控制器实现

创建一个 REST 控制器以提供 API 接口:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/activities")
public class UserActivityController {

    @Autowired
    private UserActivityService userActivityService;

    @PostMapping
    public UserActivity createActivity(@RequestBody UserActivity activity) {
        return userActivityService.saveActivity(activity);
    }

    @GetMapping("/{userId}")
    public List<UserActivity> getActivities(@PathVariable String userId) {
        return userActivityService.getActivitiesByUserId(userId);
    }
}

5.6 单元测试实现

现在,我们开始为上述服务和控制器编写单元测试。

5.6.1 服务层单元测试
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.List;

public class UserActivityServiceTest {

    @Mock
    private UserActivityRepository userActivityRepository;

    @InjectMocks
    private UserActivityService userActivityService;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testSaveActivity() {
        UserActivity activity = new UserActivity();
        activity.setId("1");
        activity.setUserId("user1");
        activity.setAction("login");

        when(userActivityRepository.save(activity)).thenReturn(activity);

        UserActivity savedActivity = userActivityService.saveActivity(activity);
        assertEquals("user1", savedActivity.getUserId());
        verify(userActivityRepository, times(1)).save(activity);
    }

    @Test
    public void testGetActivitiesByUserId() {
        UserActivity activity1 = new UserActivity();
        activity1.setUserId("user1");

        UserActivity activity2 = new UserActivity();
        activity2.setUserId("user1");

        when(userActivityRepository.findByUserId("user1"))
                .thenReturn(Arrays.asList(activity1, activity2));

        List<UserActivity> activities = userActivityService.getActivitiesByUserId("user1");
        assertEquals(2, activities.size());
        verify(userActivityRepository, times(1)).findByUserId("user1");
    }
}
5.6.2 控制器层单元测试

在控制器层的单元测试中,我们需要确保 REST API 的正确性。通过模拟服务层的行为,我们可以测试控制器对请求的处理是否正确。

控制器层单元测试示例

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Arrays;

public class UserActivityControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Mock
    private UserActivityService userActivityService;

    @InjectMocks
    private UserActivityController userActivityController;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(userActivityController).build();
    }

    @Test
    public void testCreateActivity() throws Exception {
        UserActivity activity = new UserActivity();
        activity.setId("1");
        activity.setUserId("user1");
        activity.setAction("login");

        when(userActivityService.saveActivity(any(UserActivity.class))).thenReturn(activity);

        mockMvc.perform(post("/api/activities")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"userId\":\"user1\", \"action\":\"login\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.userId").value("user1"))
                .andExpect(jsonPath("$.action").value("login"));
    }

    @Test
    public void testGetActivities() throws Exception {
        UserActivity activity1 = new UserActivity();
        activity1.setUserId("user1");
        activity1.setAction("login");

        when(userActivityService.getActivitiesByUserId("user1")).thenReturn(Arrays.asList(activity1));

        mockMvc.perform(get("/api/activities/user1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[0].userId").value("user1"))
                .andExpect(jsonPath("$[0].action").value("login"));
    }
}

解释

  1. **MockitoAnnotations.openMocks(this)**:初始化 Mock 对象,允许在测试中使用 @Mock 和 @InjectMocks 注解。
  2. **MockMvcBuilders.standaloneSetup(userActivityController)**:创建一个独立的 MockMvc 实例,用于测试控制器。
  3. **when(…).thenReturn(…)**:定义 Mock 对象的行为,当调用特定方法时返回指定的值。
  4. **mockMvc.perform(…)**:模拟 HTTP 请求,并验证返回的状态和内容。

通过上述测试,确保了控制器层对 API 的正确处理,包括创建活动和获取活动的功能。


本文转载自: https://blog.csdn.net/weixin_39996520/article/details/143246205
版权归原作者 J老熊 所有, 如有侵权,请联系我们删除。

“Spring Boot 中应用单元测试(UT):结合 Mock 和 H2 讲解和案例示范”的评论:

还没有评论