# java 单元测试框架

# 简介

在软件开发过程中,编写高质量的代码是每个开发者追求的目标。单元测试作为保证代码质量的重要手段之一,它能够帮助我们验证程序中最小可测试单元的正确性。Java 作为一门成熟的编程语言,拥有丰富的单元测试工具和框架。本文将探讨 Java 单元测试的概念、重要性、实现方式以及最佳实践。

# 前言

本章介绍如何在 springboot 下进行单元测试(Unit Test),在开始阅读之前,请先阅读《小谈 Java 单元测试》文章,对测试有一个简单的了解,特别是要区分清楚什么是单元测试,什么是集成测试(Integration Test)

# 单元测试的概念

单元测试(Unit Testing)是针对软件中最小的可测试部分 —— 通常是单个方法或函数 —— 进行验证的过程。在 Java 中,一个单元测试用例用于验证一个类中的特定方法是否按预期工作。

# 单元测试的重要性

  1. 早期发现缺陷:通过在开发早期阶段进行单元测试,可以及时发现并修复代码中的错误。
  2. 提高代码质量:单元测试为代码提供了一个质量标准,确保代码在修改和扩展过程中保持稳定。
  3. 简化维护和重构:良好的单元测试覆盖率可以减少维护和重构代码时的风险。
  4. 文档和示例:单元测试可以作为代码的文档和示例,展示如何使用特定的类和方法。

# 使用

在 Spring Boot 下,Spring Boot 提供了 spring-boot-starter-test 依赖,其中包含了 JUnit、Spring Test、AssertJ、Hamcrest、Mockito 等库,这些库支持开发者编写单元测试。

例子:本地调用,模拟 Dao, Service, 数据库数据,随机对象来做单元测试,而不需要启动额外的数据库服务,redis 等中间件

模拟 DAO:

public interface UserDao {
    User findById(Long id);
}

测试 DAO:

@ExtendWith(MockitoExtension.class)
class UserDaoTest {
    
    @Mock
    private UserDao userDao;
    @Test
    void testFindById() {
        // 模拟 findById 方法,当 ID 为 1 时返回一个用户对象
        Long testId = 1L;
        User expectedUser = new User(testId, "Test User");
        when(userDao.findById(testId)).thenReturn(expectedUser);
        // 调用方法
        User user = userDao.findById(testId);
        // 验证结果
        assertThat(user, notNullValue());
        assertThat(user.getId(), equalTo(testId));
        assertThat(user.getName(), equalTo("Test User"));
    }
}

模拟 Service:

现在,我们有一个 UserService 类,它依赖于 UserDao 。我们将模拟 UserDao 来测试 UserService

@Service
class UserService {
    private final UserDao userDao;
    @Autowired
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    public User getUserById(Long id) {
        return userDao.findById(id);
    }
}
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock
    private UserDao userDao;
    @InjectMocks
    private UserService userService;
    @Test
    void testGetUserById() {
        Long testId = 1L;
        User expectedUser = new User(testId, "Test User");
        when(userDao.findById(testId)).thenReturn(expectedUser);
        User user = userService.getUserById(testId);
        // 最后断言结果
        Assertions.assertThat(user).isEqualTo(expectedUser);
    }
}

或者用实际的 SpringBootTest 数据库使用的是内存数据库 H2, 这样不用启动整个项目:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDaoTest {
    @Autowired
    private UserDao userDao;
    @Test
    @Sql(scripts = "/sql/create_tables.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(statements = "INSERT INTO `t_user`(`id`, `username`, `password`) VALUES (1, 'username:1', 'password:1');", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
    @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public void testSelectById() {
        // 查询用户
        User user = userDao.findById(1);
        // 校验结果
        Assert.assertEquals("编号不匹配", 1, (int) user.getId());
        Assert.assertEquals("用户名不匹配", "username:1", user.getUsername());
        Assert.assertEquals("密码不匹配", "password:1", user.getPassword());
    }
}

注解说明:

@ExtendWith(MockitoExtension.class) : 用于指示 JUnit 平台使用特定的扩展。在 JUnit 5 中,扩展可以提供额外的功能,如测试执行前的准备、测试执行后的清理或测试过程中的干预。

@Mock : 用于创建模拟对象(Mock object),这些对象可以模拟真实对象的行为,用于测试时替代真实的依赖。

@InjectMocks : 注解用于创建一个类的实例,并将使用 @Mock@Spy 注解的模拟对象自动注入为该实例的依赖。

@Spy : 注解用于创建一个监视对象(Spy object),与模拟对象不同,监视对象会调用真实对象的方法,同时也可以被 Mockito 控制,用于测试时部分模拟对象的行为。

@Sql : 执行 sql 语句, scripts sql 脚本, statements sql 语句, executionPhase 执行的时机,之前或之后

# 封装测试通用组件

我们在项目中封装了专门做测试的组件 scaffold-spring-boot-starter-test 用于单元测试、集成测试等等

  1. 需要连接数据库的测试类封装,不用启动整个项目,是单个测试启动类,有自己的配置

    /**
     * <p>Project: scaffold-v2 - BaseDbAndRedisUnitTest </p>
     *
     * 
     *
     * 依赖内存 DB + Redis 的单元测试
     *
     * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis
     *
     * ActiveProfiles ("unit-test"):
     * 设置使用 application-unit-test 配置文件
     *
     * Sql (scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD):
     * 每个单元测试结束后,清理 DB
     *
     * @author: Tz
     * @date: 2023/10/24 19:31
     * @since: 1.0.0
     */
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class)
    @ActiveProfiles("unit-test")
    @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public class BaseDbAndRedisUnitTest {
        @Import({
                //========== DB 配置类 ==========
                // 自己的 DB 配置类
                ScaffoldDataSourceAutoConfiguration.class,
                // Spring DB 自动配置类
                DataSourceAutoConfiguration.class,
                // Spring 事务自动配置类
                DataSourceTransactionManagerAutoConfiguration.class,
                // Druid 自动配置类
                DruidDataSourceAutoConfigure.class,
                // SQL 初始化
                SqlInitializationTestConfiguration.class,
                //========== MyBatis 配置类 ==========
                // 自己的 MyBatis 配置类
                ScaffoldMybatisAutoConfiguration.class,
                // MyBatis 的自动配置类
                MybatisPlusAutoConfiguration.class,
                //========== Redis 配置类 ==========
                // Redis 测试配置类,用于启动 RedisServer
                RedisTestConfiguration.class,
                // Spring Redis 自动配置类
    //            RedisAutoConfiguration.class,
                // 自己的 Redis 配置类
                ScaffoldRedisAutoConfiguration.class,
                // Redisson 自动高配置类
                RedissonAutoConfiguration.class,
        })
        public static class Application {
        }
    }
  2. 依赖内存 DB 的单元测试, 相当于用 H2 做测试

    /**
     * <p>Project: scaffold-v2 - BaseDbUnitTest </p>
     * 依赖内存 DB 的单元测试
     *
     * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法
     *
     * ActiveProfiles ("unit-test"):
     * 设置使用 application-unit-test 配置文件
     *
     * Sql (scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD):
     * 每个单元测试结束后,清理 DB
     * @author: Tz
     * @date: 2023/10/24 19:31
     * @since: 1.0.0
     */
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
    @ActiveProfiles("unit-test")
    @Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
    public class BaseDbUnitTest {
        @Import({
                //========== DB 配置类 ==========
                // 自己的 DB 配置类
                ScaffoldDataSourceAutoConfiguration.class,
                // Spring DB 自动配置类
                DataSourceAutoConfiguration.class,
                // Spring 事务自动配置类
                DataSourceTransactionManagerAutoConfiguration.class,
                // Druid 自动配置类
                DruidDataSourceAutoConfigure.class,
                // SQL 初始化
                SqlInitializationTestConfiguration.class,
                //========== MyBatis 配置类 ==========
                // 自己的 MyBatis 配置类
                ScaffoldMybatisAutoConfiguration.class,
                // MyBatis 的自动配置类
                MybatisPlusAutoConfiguration.class,
                // MyBatis 的 Join 配置类
                MybatisPlusJoinAutoConfiguration.class,
        })
        public static class Application {
        }
    }
  3. 依赖内存 Redis 的单元测试

    /**
     * <p>Project: scaffold-v2 - BaseRedisUnitTest </p>
     *
     * 依赖内存 Redis 的单元测试
     *
     * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis
     *
     * ActiveProfiles ("unit-test"):
     * 设置使用 application-unit-test 配置文件
     * @author: Tz
     * @date: 2023/10/24 19:31
     * @since: 1.0.0
     */
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class)
    @ActiveProfiles("unit-test")
    public class BaseRedisUnitTest {
        @Import({
                //========== Redis 配置类 ==========
                // Redis 测试配置类,用于启动 RedisServer
                RedisTestConfiguration.class,
                // Spring Redis 自动配置类
                RedisAutoConfiguration.class,
                // 自己的 Redis 配置类
                ScaffoldRedisAutoConfiguration.class,
                // Redisson 自动高配置类
                RedissonAutoConfiguration.class,
        })
        public static class Application {
        }
    }

** 使用:** 只需要在需要测试的类集成对应的测试内容即可写自己实际需要测试的逻辑