首页 Java单元测试实践
文章
取消

Java单元测试实践

1. 背景

单元测试的收益

  1. 单元测试能更快地发现问题
  2. 单元测试的性价比很高,因为发现错误越晚,修复的代价越高
  3. 有助于源码的优化,可以放心进行重构

单元测试的痛点

  1. 单元测试浪费了太多的时间,写单元测试的时间比写代码的时间还长
  2. 代码逻辑过于复杂,单元测试很难写
  3. 部分项目主要和数据库交互,造数据复杂

测试金字塔 测试金字塔

2. Spock

Spock 是一个 Java 和 Groovy 应用程序的测试框架。Spock 结合 Groovy 动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言 让编写测试代码更加简洁、高效。

Junit 单纯用于测试,不提供 Mock 功能。Mockito 虽然可以把接口等依赖屏蔽掉,但是他们之间需要整合,语法繁琐。

Spock 自带 Mock 功能,使用简单方便,并且提供了规范化的描述,定义了多种标签(given、when、then、where 等),去描述代码 ”应该做什么“, “输入条件是什么”,“输出是否符合预期”,从语义层面规范代码的编写。再加上 Groovy 动态语言的强大语法,能写出简洁的测试代码。

3. 内存数据库H2

H2 非常适合在测试程序中使用,程序关闭时自动清理数据,H2 数据库表结构和表数据可以通过spring.datasource进行指定。单元测试中使用非常地简单, 仅需要修改 jdbc 连接即可。

引入依赖

1
2
3
4
5
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>

数据源连接

1
2
3
4
5
6
7
spring.datasource.druid.url=jdbc:h2:mem:goods;INIT=CREATE SCHEMA IF NOT EXISTS goods\\;SET SCHEMA goods
spring.datasource.druid.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# 开启控制台需要启动web
spring.h2.console.enabled=true
spring.h2.console.path=/h2

数据初始化

1
2
3
spring.datasource.schema=classpath:schema.sql
spring.datasource.data=classpath:data.sql
spring.datasource.initialization-mode=always

某些 MySQL 语法对于H2来说是不支持的。 初始化 H2 的 Schema 最好能给每个 not null 字段都加上默认值。

4. 数据层单元测试框架 DbUnit

DbUnit 基于 xml 优雅地构造测试数据集,例如:

1
2
3
4
5
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
  <user id="1" username="test-user1" password="test-user1" name="test-user1"/>
  <user id="2" username="test-user2" password="test-user2" name="test-user2"/>
</dataset>

user代表表名,后面的键值对为列名和对应的值。 但是通过xml构造数据集还是太麻烦,加入对于 Spock 扩展的处理框架,使用注解的方式构造测试数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ExtensionAnnotation(DbunitExtension)
@interface Dbunit {

    /**
     * <pre>
     *   @Dbunit(content = {
     *           t_goods_price_discount(goods_attr_id: 1, tenancy_threshold: 1, price_discount: 10)
     *           t_goods_price_discount(goods_attr_id: 2, tenancy_threshold: 2, price_discount: 20)
     *           T_GOODS_PRICE (goods_attr_id: 1, start_date: '2022-03-18', end_date: '2022-03-25', price: 10, km_price: 20, min_days: 4)
     *   })
     * </pre>
     */
    Class<? extends Closure> content() default Closure.class;

    String schema() default "";
}

使用案例

5. 单元测试覆盖率

在pom文件里引用Jacoco的插件:jacoco-maven-plugin,然后执行mvn package 命令, 成功后会在target目录下生成单元测试覆盖率的报告,点开报告找到对应的被测试类查看覆盖情况。

6. Groovy在单元测试相比于Java的优势

  1. 在单元测试环节,可以对一些访问控制权限进行放开,例如,构造函数中指定字段名和字段值的方式创建对象,更加的方便、简洁。 构造函数

参考

  1. Spock单元测试框架介绍以及在美团优选的实践
  2. 有赞单元测试实践
  3. 老K的Java博客-Spock系列
  4. @Dbunit注解的实现1
  5. @Dbunit注解的实现2
本文由作者按照 CC BY 4.0 进行授权

值得收藏的软件

Eureka源码01-EurekaServer初始化