相关链接

  1. 访问 https://maven-local.j-youxuan.com/#browse/browse:maven-public:top%2Fyjp%2Fjtf 下载最新版本。

  2. 访问 https://jtf-local.j-youxuan.com/tools/ 使用工具生成JSON 和 H2 数据库建表语句。

  3. 访问 https://jtf-local.j-youxuan.com/template.1.2.0.xlsx 下载 1.2.0 版本的Excel模板文件(CheckList和TestData合并为TestData版)。

1. 发布日志

  • 版本 1.2.3

    1. fix: 修正当DefaultObjectData对象未配置属性值(即该属性是JavaBean且值是null)的时候,而MockData又指定了要更新该属性对象(null值且是JavaBean)的属性字段时,将自动使用class#newInstance()给该属性字段赋值,之后再设置相应新属性值。

  • 版本 1.2.2

    1. feat: 增加Jodd项目内自定义Enum类型属性的不规范Setter方法的兼容性,允许Setter方法是String类型。

  • 版本 1.2.1

    1. feat: 内置Jodd项目自动解析Enum类型属性到数据库字段转换。

    2. fix: 修正Jodd项目中因配置不生效导致FastJson2解析JDateTime类型失败的问题。

  • 版本 1.2.0

    1. feat: 将Excel的CheckList和TestData表合并为TestData表,并精简字段。

    2. feat: [TestData]表增加[DisableMe]字段作为开发过程中的调试字段。

    3. feat: [MockData]表增加[MockMe]字段作为开发过程中的调试字段。

  • 版本 1.1.48

    1. fix: 修复将字符串解析为java.util.Date时,同时支持解析为日期和时间。

  • 版本 1.1.47

    1. feat: SpringBoot项目增加MyBatisPlus的Java枚举类型转换为数据库兼容字段值的支持。参见 MyBatisPlus的Java枚举类型转换为数据库兼容字段值

  • 版本 1.1.46

    1. feat: 增加在mock的时候使用DefaultObjectData表定义的默认值方式简化mock返回值的JSON代码字符串功能。

    2. doc: 更新生成Java JSON 的 Java 示例代码。参见 如何快速生成JSON和建表H2SQL

  • 版本 1.1.45

    1. feat: 增加生成Java JSON 及 创建数据库表结构导出为 H2 兼容sql 的 Java 示例。参见 如何快速生成JSON和建表H2SQL

  • 版本 1.1.44

    1. feat: 简单变量增加了对数据库数据表达式的支持。参见 模板变量语法参考 简单变量

    2. feat: 新增加自定义函数 first()json() 。参见 框架内置函数

  • 版本 1.1.43

    1. feat: 简单变量除了Java基本类型,增加了对象的属性路径取值的支持(例如:${user.address.province.name} )。参见 自定义变量 简单变量

    2. feat: 如何在单元测试的Excel文件中引用表【TestData】内的参数值 。使用内置变量 ${params.参数列英文名.对象属性路径} 的方式。参见 如何在Excel内引用表【TestData】内的参数值

  • 版本 1.1.42

    1. feat: 如何在Jodd项目中继续使用 @PetiteInject 注解简化代码且更符合该项目开发人员代码书写的习惯。参见 如何在Jodd项目单元测试类中继续使用 @PetiteInject 注解

    2. docs: 在Jodd项目单元测试类中使用 @Mock 注解的注意事项。参见 在Jodd项目单元测试类中使用 @Mock 注解的注意事项

  • 版本 1.1.41

    1. feat: 增加了使用redis的环境初始化功能(采用Java代码提供简易API方式),参见 如何在单元测试环境使用单元测试专用的redis数据库

  • 版本 1.1.40

    1. feat: 增加了返回值类型是void和Void的方法mock支持(包括普通方法及静态方法)。

  • 版本 1.1.39

    1. feat: 增强了同名且带有可变参数的方法mock的识别,同时mock参数增加如下any类型 any()|anyInt()|anyLong()|anyFloat()|anyDouble()|anyString() 的识别。

  • 版本 1.1.38

    1. feat: 在新增断言表达式求值,例如:Company.departments[name="后勤部"].employees[name="李四"].id==4L 。参考 断言表达式语法参考语法示例

    2. doc: Jodd项目使用JDateTime属性字段,实体类指定了表名后无法生成正确的 INSERT SQL 语句问题 [撰写日期: 2024-07-29]

  • 版本 1.1.37

    1. feat: 使用线程工具方法 CompletableFuture.runAsync (增加带有1个参数和带2个参数的重载方法的支持)。参考 如何使得已经mock的静态方法在线程中也能正确生效

    2. doc: 如何在单元测试中捕获调用第三方SDK方法时传递的参数值并验证参数传递是否符合预期?参考 如何在单元测试中捕获调用第三方SDK方法时传递的参数值并验证参数传递是否符合预期

  • 版本 1.1.36

    1. feat: 在使用线程工具方法 CompletableFuture.runAsync 运行的异步线程中,使用线程模拟的方式允许已Mock的静态方法在线程中生效。参考 如何使得已经mock的静态方法在线程中也能正确生效

  • 版本 1.1.35

    1. fix: 在使用@SpringBootTest注解后再使用 @MockBean 注解标注要Mock的对象,当被Mock方法参数值是基础类型(数字,字符串等Java基本类型)列表的时候,无法正确解析该参数值为java.util.List的错误。

  • 版本 1.1.34

    1. feat: 当要Mock的方法是重载方法(具有同名,参数个数相同,但参数类型不同),通过在Excel的MockData表增配 `ParameterTypes`列,指定参数的完整类型名,即可准确的让系统知道你要Mock的是哪个方法,若不明确指定,则会取第1个找到的方法。参考 如何明确指定Mock哪个重载/同名方法

    2. fix: 当项目类型是 maven 的时候,无法扫描到 /target/classes 下面的业务代码数据库实体类的问题。参考 使用DbExpect时如何要系统自动识别数据库实体类

  • 版本 1.1.33

    1. feat: 允许使用JavaBean的多级属性求值的方式进行断言。例如:User.company.name=="巨星行动"User[name="于金平"].company.name=="巨星行动" 。参考 断言表达式语法参考语法示例

  • 版本 1.1.32

    1. fix: 修复多个测试用例在执行Mock操作时使用动态简单变量作为参数或返回值时,该变量永远被解析为执行第一个用例时的取值问题。

  • 版本 1.1.31

    1. fix: 修复默认值 DefaultObjectData 未定义时, DefaultObjectMaker 依旧复制指定变动字段的问题。

  • 版本 1.1.30

    1. feat: 在断言对象 validator 增加方法 addVar(String name, Object value), 用于在断言对象 validator 中添加自定义变量。详细参考 如何添加断言自定义变量

    2. feat: 断言表达式操作符的左侧表达式允许使用基础类型值及自定义的变量。参考 断言表达式语法参考语法示例

  • 版本 1.1.29

    1. fix: 修复当在 DefaultObjectData Excel表中未定义默认对象类的默认值时,依旧返回该对象 Class 新的实例的问题。修正后,未定义默认值的类通过DefaultObjectMaker创建默认值对象时将返回null值。该问题影响 mock 泛型对象时的返回结果。请升级jtf框架到 1.1.29 版本。

  • 版本 1.1.28

    1. fix: 修复在复杂场景下 throw:: 语法抛出异常不稳定的问题。

    2. doc: 文档增加跳转链接到相应的章节,方便在 PDF 文档里直接跳转。

  • 版本 1.1.27

    1. feat: 增加 mock 的方法抛出异常的语法支持 ,参见 【如何模拟(mock)一个方法抛出指定的异常(Exception)】表格内的语法 throw::

  • 版本 1.1.26

    1. doc: 文档增加【Mock表达式语法参考】章节,用于介绍单元测试框架的Mock表达式语法。

    2. feat: 增加 mock 参数是 JavaClass 的类型定义语法,参见 【如何让 Mock 方法返回一个已知的 mock 对象】表格内的语法 class::

    3. feat: 增加 mock 返回值是已 mock 属性的定义语法,参见 【如何让 Mock 方法返回一个已知的 mock 对象】表格内的语法 ref::

  • 版本 1.1.25

    1. doc: 文档增加【疑难杂症问答】章节,用于收录各个项目使用框架过程中遇到的问题及其解决方法。

    2. feat: 增加 top.yjp.jtf.core.junit.MockHook 接口,用于解决 SpringBoot 单元测试环境下Mock MybatisPlus的 Mapper 接口时,使用 MybatisPlus LambdaQuery报错的解决方案。详细参见 【疑难杂症问答】章节

  • 版本 1.1.23

    1. feat: 增加 @BeforeMock 注解,允许明确指定 Mock 的方法返回类型,并在文档中给出例子。

    2. fix: 修正当返回方法需要的是 Set<T>时返回的是 List<T>的错误,需要使用 @BeforeMock 注解明确指定返回类型。

    3. feat: 断言表达式增加==符号右边可以是基础数组常量的语法支持。例如: 断言猫咪有 2 个名字,Cat.names==["Iris","Andy"]。参考【断言表达式语法参考】。

  • 版本 1.1.22

    1. feat: 增强 SpringBoot 使用 SpringBootTest 的 @MockBean 注解的使用

  • 版本 1.1.21

    1. feat: 添加了静态方法的 Mock 支持

  • 版本 1.1.20

    1. feat: 新增支持在单元测试代码中定义自定义变量,并且支持在 Excel 表中使用该自定义变量功能。自定义变量的功能参见 【断言表达式语法】 的【自定义变量】章节。

    2. fix: 修复当复杂嵌套对象作为 Mock 的参数时,丢失部分属性的问题

2. 运行环境说明

单元测试框架的 Java 运行时使用的是jdk8或openjdk8。其他版本的 jdk 只需要修改所依赖项的版本适配你的项目支持的 Java 环境即可。 所需要的依赖及其所对应的 JDK 版本,可以从 Maven 仓库 查询。 需要的 jtf (JUnit Test Framework)单元测试框架可以从 巨星 Maven 仓库 下载。

当前 jtf(JUnit Test Framework)单元测试框架版本 jtf-version: 1.2.3!

3. 基本项目配置

3.1. Maven项目

修改你的 Maven 项目的 pom.xml, 增加 JUnit 单元测试环境支持。
依赖的 JUnit 版本必须是 5.10.1 及以上版本。若你使用的 SpringBoot 或 Jodd 项目中引入了其他低版本的 JUnit 请先从项目中排除。
通常 JUnit 及其依赖版本不匹配或冲突会导致框架运行出现莫名其妙的问题。

3.1.1. pom.xml

可根据项目实际情况将如下 pom.xml 修改要点中的配置信息加入到你的 Maven 项目中。
Example 1. pom.xml 修改要点
<project>
<properties>
    <java.version>8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

    <!-- 单元测试依赖 jar 包 -->
    <junit.version>5.10.1</junit.version>
    <junit-jupiter.version>5.10.1</junit-jupiter.version>
    <mockito.version>4.11.0</mockito.version>
    <easyexcel.version>3.3.3</easyexcel.version>
    <reflectasm.version>1.11.9</reflectasm.version>
    <byte-buddy.version>1.14.10</byte-buddy.version>
    <antlr4.version>4.9.3</antlr4.version>
    <fastjson.version>2.0.45</fastjson.version>
    <caffeine.version>2.9.3</caffeine.version>
    <h2database.version>2.2.224</h2database.version>
</properties>
<dependencies>
<!-- SpringBoot项目开始 -->
<!-- 添加 SpringBoot 测试支持,并排除 JUnit4 相关的类,防止类冲突  -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
<!-- SpringBoot项目结束 -->
<!-- 单元测试框架依赖开始 -->
        <!-- 添加 JUnit 支持,必须使用 5.10.1 及以上版本 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- 添加 Mock 的支持 -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>${mockito.version}</version>
            <exclusions>
                <!-- 排除 mockito-junit-jupiter 内的 mockito-core -->
                <exclusion>
                    <groupId>org.mockito</groupId>
                    <artifactId>mockito-core</artifactId>
                </exclusion>
            </exclusions>
            <scope>test</scope>
        </dependency>
        <!-- 将 mockito-core 替换为 mockito-inline ,用于支持静态方法的 Mock -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>${byte-buddy.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>${byte-buddy.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- 测试框架使用 Excel 作为参数化数据源,故而依赖EasyExcel 包 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>${easyexcel.version}</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.esotericsoftware</groupId>
            <artifactId>reflectasm</artifactId>
            <version>${reflectasm.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- 添加内存数据库 h2database 支持,使用兼容 MYSQL 的内存模式 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${h2database.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>${caffeine.version}</version>
        </dependency>
<!-- 单元测试框架依赖结束 -->
</dependencies>
</project>

3.1.2. 配置文件

准备如下文件:

  1. SpringBoot测试环境的配置文件:${project.basedir}/src/test/resources/application.properties${project.basedir}/src/test/resources/application.yml

Example 2. application.properties 配置
# ---------数据源配置-----------
spring.datasource.driver-class-name=org.h2.Driver
# MODE=MYSQL 配置开启MYSQL语法兼容模式,IGNORECASE=忽略表及字段的大小写,DB_CLOSE_ON_EXIT=单元测试完成后关闭内存数据库,NON_KEYWORDS=将列出的关键词不视为关键字
spring.datasource.url=jdbc:h2:mem:unit_demo;DB_CLOSE_DELAY=-1;IGNORECASE=TRUE;DB_CLOSE_ON_EXIT=TRUE;MODE=MYSQL;NON_KEYWORDS=USER
spring.datasource.username=sa
spring.datasource.password=

# SQL脚本配置项
spring.sql.init.platform=h2
spring.sql.init.encoding=UTF-8
# 初始化模式,always=每次都重新初始化数据库
spring.sql.init.mode=always
# 如果在初始化数据库时发生错误,是否停止
spring.sql.init.continue-on-error=false

# ---------- H2 控制台配置 开启后可以通过本地 WEB访问数据库 --------------
spring.h2.console.path=/h2
# 是否启用控制台
spring.h2.console.enabled=true

3.1.3. 资源文件

准备数据库初始化的资源文件

  • h2database的数据库初始化SQL文件:数据库结构SQL文件( src/test/resources/schema.sql ) 和 数据库数据SQL文件( src/test/resources/data.sql )

  • 若你没有 SQL 数据的话,可以不创建 data.sql,但若有该文件,则内容不允许为空。

  • 这 2 个文件可以合二为一 /test/resources/schema.sql,但是注意 *.sql 文件不可以内容为空,因此你可以建一个简单的临时表并写入 1 条数据即可。

Example 3. schema.sql
CREATE TABLE `tmp_table_h2` (
   `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
   `name` varchar(255) NOT NULL COMMENT '名称',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 COMMENT='临时表(切勿删除)';
Example 4. data.sql
INSERT INTO `tmp_table_h2`(`name`,`create_time`)VALUES ('测试数据',now());

此时,h2数据库在单元测试框架加载后将自动加载 classpath 内的 schema.sqldata.sql 文件作进行内存数据库结构的创建和数据的初始化。 schema.sqldata.sql 的具体内容可以根据项目具体进行填充。 至此,SpringBoot项目(Maven)使用内存数据库方式进行单元测试环境配置大功告成。

4. 单元测试框架

4.1. 安装

在前述基础之上再次修改你的 Maven 项目的 pom.xml, 增加自研发单元测试环境支持。 源码位于:https://gitlab.j-youxuan.com/testing/junit_test_framework.git 若你可以使用(巨星Maven仓库) 则直接 pom.xml 增加如下依赖,${jtf-version} 使用最新的 1.2.3

    <dependency>
      <groupId>top.yjp.jtf</groupId>
      <artifactId>jtf-spring</artifactId>
      <version>${jtf-version}</version>
      <scope>test</scope>
    </dependency>

4.2. 配置

单元测试框架使用了 Java SPI 的机制作为默认的配置方式。确保你的测试源码所在的资源文件夹有 /META-INF/services 这个文件夹。 然后在该文件夹内创建 properties 配置文件,文件内容的编码请使用`utf-8`的编码方式,否则会出现中文乱码的情况。 jtf单元测试框架需要如下 2 个配置文件

  • src/test/resources/META-INF/services/top.yjp.jtf.core.junit.factory.BeanFactory.properties

  • src/test/resources/META-INF/services/top.yjp.jtf.core.junit.InitFactory.properties

框架配合 SpringBoot 需要包括如下配置,然后根据各个平台的情况进行添加、修改或删除

Example 5. 示例配置文件[top.yjp.jtf.core.junit.factory.BeanFactory.properties]
# 通常情况下使用 SpringBoot +MyBatisPlus 的项目使用如下 2 行配置即可
top.yjp.jtf.core.db.DbTable=top.yjp.jtf.ext.spring.db.mybatisplus.MyBatisPlusDbTable
top.yjp.jtf.core.db.SqlGenerator=top.yjp.jtf.ext.spring.db.mybatisplus.MyBatisPlusSqlGenerator

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# 如下配置主要用于 jodd 项目
# top.yjp.jtf.core.db.DatabaseOperator=top.yjp.jtf.core.db.jdbc.JdbcDbOperator
# top.yjp.jtf.core.db.DbTable=top.yjp.jtf.ext.jodd.db.JoddDbTable
# top.yjp.jtf.core.db.SqlGenerator=top.yjp.jtf.ext.jodd.db.JoddSqlGenerator
# top.yjp.jtf.core.junit.ClassId=top.yjp.jtf.core.junit.id.DefaultClassId
# top.yjp.jtf.core.junit.DbBuilder=top.yjp.jtf.core.junit.dbbuilder.DefaultExcelFileDbBuilder
# top.yjp.jtf.core.junit.Maker=top.yjp.jtf.core.junit.obj.DefaultObjectMaker
# top.yjp.jtf.core.junit.Mocker=top.yjp.jtf.core.junit.mock.DefaultExcelFileMocker
Example 6. 示例配置文件[top.yjp.jtf.core.junit.InitFactory.properties]
# 启动初始化的配置,需要该类实现 InitFactory 接口
# 通常情况下,SpringBoot 项目不需要配置任何 InitFactory,即 top.yjp.jtf.core.junit.InitFactory.properties 存在即可
#
# Jodd 项目需要增加 JoddInitFactory 作为启动工厂类
# JoddInitFactory=top.yjp.jtf.core.junit.JoddInitFactory

# 对于使用 DbExpect 列进行断言的时候,如果出现找不到 Class 的时候,请加入如下配置
# ScanClassInitFactory =top.yjp.jtf.core.junit.factory.ScanClassInitFactory

4.3. 编写单元测试

4.3.1. 准备测试清单 Excel 文件

该Excel文件包含如下表格,并与你的单元测试代码放置同一个文件夹下

  • CheckList 测试清单表,即第 1 张表,主要是依据既有格式列出本次单元测试时所涉及到的概念或对象等。用于厘清单元测试时各个检验点,为测试及测试数据做准备,这是写单元测试前的重点,尽量能够写全。

  • TestData 测试数据表,即第 2 张表,主要存放本次单元测试的测试数据。

  • DbData 数据库初始化表,即第 3 张表,主要存放本次单元测试的数据库初始化数据。

  • MockData Mock 数据表,即第 4 张表,主要存放本次单元测试的 Mock 数据信息。

  • DefaultObjectData 默认对象数据表,即第 5 张表,主要存放本次单元测试的对象的默认实例提供。

Excel各个表的格式如下面截图所示:

CheckList 测试清单表

CheckList 截图

TestData 测试数据表

最重要的列 DbExpect 和 Expect ,存储本次单元测试的断言列表,每个断言一行,目前只支持断言相等(==)和断言不等(!=)。

  1. DbExpect 用于直接断言数据库中的表数据是否符合预期。此时无需将数据库数据取出放到内存中。

  2. Expect 列用于断言内存中的数据是否符合预期。当然,你可以把内存中数据表的数据通过程序取出来后再使用 Expect 列进行断言操作也是可以的。

  3. 二者的执行顺序是先使用 Expect 断言内存数据,然后再通过 DbExpect 断言内存数据库数据。

  4. 对于单条数据,写法格式:要断言的实体类名称.属性名==预期值。

  5. 对于 List 类型的数据,写法格式:要断言的实体类名称[业务主键字符串或过滤条件].属性名==预期值。写法格式中的业务主键通常是该实体类的 ID。

  6. 如果在编写 TestData 的过程中,想对某些测试数据进行测试,而另外一些暂时不测试。可以在 TestData 表增加一列名为 TestMe 的列,取值 是/否 或 Y/N。即可实现。

  7. 更为详细的用法参考断言表达式语言章节。

TestData 截图

DbData 数据库初始化表

DbData 截图

MockData Mock 数据表

MockData 截图

方法的参数列表 Parameters :

  • 参数列表内的参数两应与要 Mock 的方法签名一致

  • 参数类型仅支持Java基本类型及 List 和 Map(List 和 Map 的 Key 值及 Value 值都只支持基本类型)。

  • 参数是 List 类型时,使用[]将实参值包裹。

  • 参数是 Map 类型时,使用{}将实参值包裹。

  • 参数是数字时,使用数字+类型后缀方式(与 Java 语言本身相同),例如:add(Long param1,Long param2),则可 mock 为 add(1L,2L) Long 类型后缀是 L, Double 类型后缀是 D,Float 类型后缀是 F。

  • 对于不介意该参数值的情形,该参数值用 any()表示。

  • 对于参数需要传入 null 值的,则该参数值用带有双引号的 "null" 表示。

  • 对于 mock 的方法的调用,只有输入参数与 Excel 表内定义的输入参数值严格一致的时候,才会真正调用到已 Mock 的方法。

方法的返回类型 ReturnType :

  • 通常返回的是 Java 对象,要求填写该类的完整名称。

  • 当返回的是带有泛型的List时,直接填写泛型类的完整名称。

  • 当返回的是 Map 类型时,直接填写 Map 的 Key 和 Value 的类型,中间用英文逗号分隔。

  • 随着版本迭代的发展,当你使用的 jtf版本 >=1.1.25 的时候,通常情况下无需填写该列,jtf框架将根据要mock的方法签名自动推断返回类型。

  • 对于要 Mock 的方法其返回值是带有泛型的复杂的 Map 或 Bean 时,例如:方法返回类型是Map<Long, Map<String, ProductDto>>,由于 Java的泛型擦除机制,对于深度泛型的难于反射。因此你可以使用 @BeforeMock 注解在测试类新增类似如下代码方法

// 示例代码中的 TypeReference 类型是 com.alibaba.fastjson2.TypeReference,若存在对应的 TypeReference 则会优先使用该 TypeReference 来作为泛型的返回类型,即使用 TypeReference<>的<>内的Map<Long, Map<String, ProductDto>>作为返回值 JSON的类型。
    @BeforeMock
    public void beforeMock(Mocker mocker) {
        // skuProductService.queryItemProductQuantity 是 Excel 中 MockData 表内 MockMethod 列的值
        mocker.setMockMethodReturnType("skuProductService.queryItemProductQuantity", new TypeReference<Map<Long, Map<String, ProductDto>>>() {});
    }

MockData 截图

DefaultObjectData 默认对象数据表

在 DefaultObjectData 表定义好默认对象后,意味着你的 MockData 和 DbData 两张表内的对象的 JSON 将可以得到很大程度上简化。 在 MockData及 DbData 表中的 JSON 数据可以只填写关注的且不同与默认值的属性值即可,有助于增加 Excel 内容的可阅读性! 同时若你想在单元测试类中是用 DefaultObjectData 表定义的默认值的话,你只需要是使用代码如下代码即可获得一个预定义的默认值对象。

 MyBean defaultInstance = this.maker.make(MyBean.class);

DefaultObjectData 截图

4.3.2. 进行单元测试编码(基础示例代码)

Example 7. BastTest.java (示例的测试基础类)
package top.yjp.testing;

import top.yjp.testing.annotation.ExcelInject;
import top.yjp.testing.db.DataSourceProvider;
import top.yjp.testing.db.DatabaseOperator;
import top.yjp.testing.db.provider.H2DataSourceProvider;
import top.yjp.testing.junit.DbBuilder;
import top.yjp.testing.junit.Maker;
import top.yjp.testing.junit.Mocker;
import top.yjp.testing.junit.Validator;

import javax.sql.DataSource;

public class BaseTest {
    @ExcelInject
    protected DbBuilder builder;
    @ExcelInject
    protected Mocker mocker;
    @ExcelInject
    protected Maker maker;

    @ExcelInject
    protected DatabaseOperator operator;
    @ExcelInject
    protected Validator validator;

    private DataSource dataSource=null;
    @ExcelInject
    public DataSource getDataSource(){
        if(this.dataSource==null){
            // 此处可以替换为各个项目的获取数据源的实现
            String url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;IGNORECASE=TRUE;DB_CLOSE_ON_EXIT=TRUE;MODE=MYSQL";
            String user = "sa";
            String password = "";
            H2DataSourceProvider provider = new H2DataSourceProvider();
            provider.setUrl(url);
            provider.setUser(user);
            provider.setPassword(password);
            this.dataSource =provider.getDataSource();
        }
        return this.dataSource;
    }
}

通常情况下,一个 jtf 测试类结构如下所示:

Example 8. AntlrExpressionTest.java
package top.yjp.testing.antlr4;
@Slf4j
@ExcelConfiguration
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExtendWith(JTFExtension.class)
@DisplayName("ANTLR4断言表达式测试")
// ⚠️注意:请将 @ExtendWith(JTFExtension.class) 放在最靠近方法声明的行或位于 @ExtendWith(MockitoExtension.class) 之下
// 无论是否需要 Mock 都请按照如上固定格式使用注解
public class AntlrExpressionTest extends BaseTest {
    @Mock
    private MockOrderService mockOrderService;
    @InjectMocks
    private MockOrderLogic mockOrderLogic;

    @TestTemplate
    @DisplayName("使用Excel: 1次测试所有断言表达式")
    public void testAllExpressionsFromExcelFile(Map<String, String> map) {
        if(map==null){
            throw new RuntimeException("Excel初始化错误!");
        }
        log.info("{}",map);
        // 此处开始写业务测试代码并将业务返回对象或捕获的业务异常添加到 validator 对象
        // 添加 Exception 对象
        RuntimeException re = new RuntimeException("运行时错误");
        validator.addSingleObject(re);
        // 执行业务代码测试及
        this.validator.validate();
    }
}

4.4. Mock 的使用

对于 SpringBoot 的应用,默认情况下,Mock 支持如下注解,

  • @Mock 用于产生mock 对象,最常用的Mock注解。你可以用来 Mock 普通类内的 public 方法。

  • @MockBean 这是一个 SpringBoot 提供的注解,用于生成一个可以自动注入的 Mock 过的 Bean。可以解决 Mockito等框架无法 Mock 接口的问题。

Mock 的示例代码:

package top.yjp.ext.spring.suite;


import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import top.yjp.ext.spring.entity.MockOrder;
import top.yjp.ext.spring.entity.User;
import top.yjp.ext.spring.mapper.UserMapper;
import top.yjp.ext.spring.service.CompanyService;
import top.yjp.ext.spring.service.MockOrderService;
import top.yjp.ext.spring.service.UserService;
import top.yjp.jtf.core.annotation.ExcelConfiguration;
import top.yjp.jtf.core.junit.extension.JTFExtension;

import java.util.List;
import java.util.Map;

/**
 * Unit test for simple App.
* 注意:由于 @ExtendWith 是个可重复注解,需要注意其注解值的顺序
* 建议:标准化按照如下的顺序书写
 */
@Slf4j
@SpringBootTest
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExcelConfiguration
@ExtendWith(JTFExtension.class)
public class MockTest extends JUnitBase {
    @Autowired
    UserMapper userMapper;
    @Autowired
    CompanyService companyService;

    // SpringBoot 项目使用 MockBean 注解标注后,Mock 时可以使用接口
    @MockBean
    private UserService userService;

    @Mock
    private MockOrderService mockOrderService;

    /**
    * 对于要 Mock 一个静态方法,需要先实例化该类,并将该实例标记为 static 的,然后再添加 @Mock 注解即可。
    */
    @Mock
    private static StringUtil stringUtil = new StringUtil();

    @TestTemplate
    public void testApp(Map<String, String> map) {
        log.info("SpringBoot Map={}", map);
        User user = new User();
        user.setId(100L);
        user.setUsername("admin");
        userMapper.insert(user);
        User db = userMapper.selectById(100L);
        Assertions.assertEquals(100L, db.getId());
        List<User> users = companyService.employees();
        validator.addListObject(users,User.class);
        validator.addListObject(mockOrderService.all(), MockOrder.class);
        validator.validate();
    }
}

4.5. 数据库

使用内存数据库时,每个用例执行之前都会重置数据库到初始状态,并将本测试用例需要的数据写入数据库。 重置数据库的过程:

清空内存数据库 → 执行schema.sql → 执行 data.sql → 执行 测试类名.sql → 执行 Excel 中DbData 定义的对象并写入数据库表

jtf 测试框架自动执行内存数据库的重置工作。 对于算法类的单元测试而言,如果你确定不访问内存数据库或者不需要每次都重置数据库,那你可以用注解 @ExcelConfiguration(dbAction = @DbAction(DbActionType.NONE)) 对测试类进行标注,标注后除了系统启动时自动的初始化动作外, 每一条测试用例执行时都将不再自动重置内存数据库,可以提高程序的运行效率。

4.5.1. 数据库数据初始化

对于数据库的初始化分为 3 部分进行。

  1. 进行全局初始化。执行数据库结构 /schema.sql 和公共数据 /data.sql(若存在的话) 这 2 个 SQL 文件。

  2. 执行与测试类同名的 SQL 文件,初始化业务测试数据。例如测试类 DemoTest.java,则会寻找同文件夹下名为 DemoTest.sql 的文件进行执行(若存在的话)

  3. 最后执行与测试类同名的Excel文件,使用 Excel 文件内的 DbData 表写数据到内存数据库。

4.5.2. 生成 schema.sql 文件

通过框架提供的 m2h.exe 应用程序(mysql2h2),可以根据配置文件 config.yml 自动导出 mysql 的架构到 h2 数据库架构的兼容版本。 config.yml 配置:

host: localhost
port: 3306
user: root
pass: test
database: test

命令行程序执行:

# windows
# 查看参数帮助
m2h.exe export -h
# 直接导出
m2h.exe export

该程序会根据配置文件自动找本地的替换文件,例如配置文件你使用的是 config.yml 的话,那么程序自动使用 config.yml.txt 作为关键字替换文件 该文件格式与 properties 文件格式一样,只是 = 两边均支持空格,如下所示

bit(1) DEFAULT b'0'=BOOLEAN DEFAULT FALSE
unsigned =
double(=decimal(
double=decimal(11,2)

若该文件存在,则文件的末尾应该是 1 个空行。程序会将 = 左边的内容替换为 = 右边的内容(包括空格)

4.5.3. 查看内存数据库的数据

使用 Web 控制台查看

当你使用 JetBrains IDEA 或 Eclipse 对测试类进行断点调试(Debug)的时候,你可以通过使用浏览器打开 http://localhost:8082 的 GUI 控制台,以便于你能够访问和查看内存数据库的数据。 使用注意事项:

  1. 假如你的项目的数据库连接 jdbc:h2:mem:unit_demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;IGNORECASE=TRUE;MODE=MYSQL,那么弹出GUI 界面里输入的 URL请将 "mem"替换成"tcp://localhost/mem",即上述 URL 变成`jdbc:h2:tcp://localhost/mem:unit_demo;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;IGNORECASE=TRUE;MODE=MYSQL`

  2. Debug 时设置的断点挂起的类型为线程。(在断点的红点上单击鼠标右键,选择【线程】或【Thread】即可,否则因为全面挂起所有进程而导致 H2WebServer 无法启动)

Debug 设置断点

测试内存数据库连接

查看内存数据库数据

使用 IDEA 数据库查看

当你使用 JetBrains IDEA 或 Eclipse 对测试类进行断点调试(Debug)的时候, 根据上述规则配置你的客户端的 url 地址(即将 URL 中 h2:mem: 替换为 h2:tcp://localhost/mem:) 后并配置账号密码后,当程序运行到断点的时候,即可访问内存数据库。

配置内存数据库 URL

查看内存数据库数据

4.6. 数据库兼容性

  1. H2数据库对于MYSQL 索引、触发器等支持不理想,安全起见建议只是导出表结构,对于索引、触发器、视图、存储过程等不建议导出到H2数据库。

  2. 导出的表字符集不兼容utf8mb4,需要将 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci 去除去除后默认是UTF8,因此除了表情符号、特殊符号等中文支持正常。

  3. MYSQL数据类型异常 bit(1) default b'0' 不被H2支持,需要转换为BOOLEAN DEFAULT FALSE。

  4. MYSQL 的 ROW_FORMAT=DYNAMIC 语句不兼容,去除即可。

  5. 主键使用索引引擎不兼容 PRIMARY KEY (id) USING BTREE,主要是去除 USING BTREE。

  6. H2不支持 unsigned 无符号的数字类型,去除 unsigned 签名。

  7. H2不支持枚举enum类型,可以固定改成varchar(80)类型。

  8. H2 数据库有一些自定义的关键字,因此建议表名及字段名使用``符号包裹

  9. H2 数据库 USER 是关键字,若你使用 USER 作为表名,首先表名用`USER`,然后连接 URL 可以添加 ;NON_KEYWORDS=USER 来解决无法查询表的问题。

  10. H2 数据库 MONTH,YEAR,WEEK 等是关键字或函数,因此若表名或字段名有使用这些关键字,可以使用``包裹;若包裹后仍未解决,则请尝试修改连接 URL,将关键字添加到 NON_KEYWORDS=列表中去。

  11. 强烈建议使用上文中提到的 m2h 工具生成 schema.sql,默认情况下 90% 以上都是兼容的,手动更改的极少。

4.7. 框架自定义注解

4.7.1. @ExcelConfiguration

用途:标注在JUnit测试类上,用于使用测试框架中对于 Excel 文件的默认配置。其默认配置可以参考源码:

package top.yjp.testing.annotation;

import top.yjp.testing.strategy.ExcelFileNameStrategy;
import top.yjp.testing.strategy.NullExcelFileNameStrategy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelConfiguration {
    /**
     * 默认的 Excel 配置信息
     * 注解到 JUnit 单元测试类上,配置各个表的相关属性
     */
    ExcelConfig[] value() default {
            @ExcelConfig(value = ExcelConfigType.TEST_DATA, fetchType = FetchType.COLUMN_NAME_MAP),
            @ExcelConfig(value = ExcelConfigType.DB_DATA, fetchType = FetchType.COLUMN_NAME_MAP),
            @ExcelConfig(value = ExcelConfigType.MOCK_DATA, fetchType = FetchType.COLUMN_NAME_MAP),
            @ExcelConfig(value = ExcelConfigType.DEFAULT_OBJECT_DATA, fetchType = FetchType.COLUMN_NAME_MAP)
    };

    // 默认值 @DbAction(DbActionType.REBUILD) 是重新构建数据库
    // @DbAction(DbActionType.NONE) 则是什么都不做,即不会重新构建数据库
    DbAction dbAction() default @DbAction(DbActionType.REBUILD);

    // 默认的 Excel 文件名生成策略,默认是空的,将会优先取 @ExcelFileSource 配置的 Excel 文件名生成策略
    // 如果@ExcelConfiguration和@ExcelFileSource中的strategy()都是空的,则会用 @ExcelFileSource的 file 等基本信息定义 Excel 文件。
    Class<? extends ExcelFileNameStrategy> strategy() default NullExcelFileNameStrategy.class;

    /**
     * 默认 Excel 文件是必须的,当你需要一次性测试(非参数化测试)的时候,又想依旧是用当前配置好的内存数据库,那么你可以将
     * excelFileRequired 设置为 false。这样将不会要求 Excel 文件存在。
     */
    boolean excelFileRequired() default true;
}

通常情况下你的 JUnit 代码将和如下代码书写方式一致(注意:MockitoExtension 必须要位于 JTFExtension 之前,因为 JTFExtension 使用了 Mockito 的相关内容):

import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@Slf4j
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExcelConfiguration
@ExtendWith(JTFExtension.class)
public class JDateTimeTest extends BaseTest {
    @TestTemplate
    public void testSaveJDateTimeToH2Database(Map<String,String> map){
        log.info("Map={}",map);
    }
}

如果你想使用一次性测试(非参数化测试),并能够访问内存数据库的话,可以按照如下示例代码进行用例编写。此时,你的测试类里可以有多个测试方法。

package top.yjp.jtf.ext.jodd.suite.time;

import jodd.datetime.JDateTime;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import top.yjp.jtf.core.annotation.ExcelConfiguration;
import top.yjp.jtf.core.db.jdbc.JdbcDbOperator;
import top.yjp.jtf.core.junit.extension.JTFExtension;
import top.yjp.jtf.core.junit.factory.DefaultInitFactory;
import top.yjp.jtf.ext.jodd.base.BaseTest;
import top.yjp.jtf.ext.jodd.base.H2DataSource;
import top.yjp.jtf.ext.jodd.db.JoddSqlGenerator;
import top.yjp.jtf.ext.jodd.entity.JoddUser;

import java.time.LocalDateTime;
import java.util.Map;

@Slf4j
@ExcelConfiguration(excelFileRequired = false)
@ExtendWith(JTFExtension.class)
public class JDateTimeTest extends BaseTest {
    @TestTemplate
    public void testSaveJDateTimeToH2Database(Map<String,String> map){
        String id = "10";
        JoddUser user = new JoddUser();
        user.setId(id);
        user.setRealName("于先生");
        user.setProvince("广东省");
        user.setCity("广州市");
        user.setRegisterTime(LocalDateTime.now());
        user.setUpdateTime(new JDateTime());
        user.setCreateTime(new JDateTime());
        user.setRegisterFrom(5);
        operator.save(user);
        JoddUser dbUser =  operator.findById(JoddUser.class,id);
        log.info("DbUser={}",dbUser);
        Assertions.assertEquals(user.getUpdateTime(),dbUser.getUpdateTime());
        Assertions.assertEquals(user,dbUser);
    }

    @TestTemplate
    public void testDemo(Map<String,String> map){
        log.info("这个类里可以写多个测试方法,并且每个方法只会执行1次,不同方法执行前仍旧会进行重置数据库等操作");
    }
}

4.7.2. @ExcelInject

用途:标注在JUnit测试类的属性或方法上,对于提供给 jtf 框架的数据源,你可以使用如下方式。

import javax.sql.DataSource;
public class Test{
    private DataSource dataSource = null;
    @ExcelInject
    public DataSource getDataSource(){
        if(this.dataSource==null){
            // this.dataSource = new ...;
        }
        return this.dataSource;
    }
}

4.7.3. @VariableInject

用途:当你的单元测试 Excel 中需要使用一些简单变量的时候,你可以使用这个注解。用于标注在JUnit测试类的方法上 ,且该方法只有1个参数并且该参数的类型是`top.yjp.jtf.core.junit.Variable`,你可以参考如下方式使用。

    @VariableInject
    public void inject(Variable variable){
        variable.addVar("company","昆山巨星行动电子商务有限公司");
        variable.addVar("today", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
    }

4.7.4. @BeforeMock

用途:当你要 Mock 的对象比较复杂,而 JTF框架推断的类型不正确的时候(通常这种情况下是因为返回值类型是嵌套的泛型类型),你可以使用该注解手动设置要 Mock 表达式的正确返回类型。 标注在JUnit测试类的方法上,要求该方法是 void 返回类型且只有1个Mocker类型参数,你可以参考如下方式使用。

    @BeforeMock
    public void beforeMock(Mocker mocker){
        // 明确设定 Mock 表达式 skuProductService.queryItemProductQuantity 是 Map<Long, Map<String, ProductDto>>
        mocker.setMockMethodReturnType("skuProductService.queryItemProductQuantity", new TypeReference<Map<Long, Map<String, ProductDto>>>() {});
        // 明确设定 Mock 表达式 stargoUtil.sendCloudDelivery2 是 HxDeliveryResp<HxDeliveryData>
        mocker.setMockMethodReturnType("stargoUtil.sendCloudDelivery2", new TypeReference<HxDeliveryResp<HxDeliveryData>>() {});
    }

4.8. 断言表达式语法参考

表达式语言采用的使用 antlr4 的 4.9.3 版本,最后一个支持 java8 的版本开发的!

每一行是一个断言,// 开头的是单行注释 ,基本语法参考 java 的 if 判别

4.8.1. 基本语法

  • [] 符号左侧是 JavaBean 类型名,[]本身代表这是个 java.util.List 的集合

  • [] 符号中的内容表示对这个JavaBean List 集合的过滤条件,允许条件的嵌套

  • [""] 表示获取这个 JavaBean List 集合里元素 id 是""内的值的元素。 例如:User["100"] 表示List 集合内 ID=100 的那个 User

  • [property=value] 符号中property=value 称为过滤条件。 property是该 JavaBean 的属性,value是希望的值,也可以是过滤表达式

  • . 符号表示取该 JavaBean 的属性,属性名是.右侧的部分

  • == 符号和 != 符号是目前支持的 2 个断言操作符,分别表示断言相等和断言不等

  • null 值(由于是 antlr4 的关键字)使用 "null" 的方式代替

  • 数字型的数字如果你需要明确该数字的类型,则其语法与 Java 语言一致。例如 Double 型的 3.5 写成 3.5D 即可。

  • () 代表这是一个函数调用,与 Java 语言的用法一致,但函数参数只支持基本数字类型、String、 List 及 any()

  • ${name} 这代表一个名称为 name 的自定义变量,可以出现在各个 Excel 表中。 若该变量未定义,则会输出null

4.8.2. 函数支持

  • size() 这个函数无参数。只能出现在[]List集合处,用于获取 List 的大小。

  • sum() 这个函数只支持 1 个参数,该参数是[]List集合元素的一个属性名,且该属性的值是数字类型,用于计算集合的合计值。

  • get***() 只能用在类名的后面,表示你可以执行该类对象的 get 方法以获取动态的值。例如:RuntimeException.getMessage()。

4.8.3. 语法示例

StockLogisticsItem[price=StockLogistics[name="测试商品",sku="SKU100"].price].price==StockLogistics[name="测试商品",sku="SKU3"].price

StockLogisticsItem[price=StockLogistics[name="测试商品",sku="SKU100"].price].sum(price)==StockLogistics[name="测试商品",sku="SKU3"].sum(price)

StockLogisticsItem[price=StockLogistics[name="商品名称:价格是100.11的测试商品SKU5"].sum(price)].price==StockLogistics[name="测试商品",sku="SKU3"].price

StockLogistics[name="测试商品"].size()==3

StockLogistics[name="测试商品",sku="SKU100"].size()==1

StockLogistics[name="没有这个测试商品",sku="SKU100"].size()==0

StockLogistics[name="测试商品"].sum(price)!=211.22

StockLogistics[name="测试商品",sku="SKU100"].sum(price)==100.11

//如下一条肯定测试不通过,没有任何一个 price 是 9.9 的,所有的测试数据的 price>=100
//StockLogistics[name="测试商品",sku="SKU100"].sum(price)==9.9

//orderId="${orderId}" 支持变量名的方式断言
StockLogistics[id=1].sum(price)==StockLogistics[id=1].price

// 测试 Class["id"].FunctionCall 形式是否正确
StockLogistics["1"].sum(price)==StockLogistics[id=1].price

StockLogistics["没有这个测试商品"].size()==0

StockLogistics.id==1

StockLogistics.id=="1"

StockLogistics.id!=2

StockLogistics.id!="2"

// 如下是 null 值计算,由于 null 是 ANTLR 的关键字,直接写会报错,故而 null 值统一用 "null"来代替
StockLogistics["没有这个测试商品"].sum(abc)=="null"
StockLogistics["没有这个测试商品"].sum(abc123)=="null"

// 断言表达式的右边可以是数组型,目前只支持数组内的元素是基本java对象(如数字、字符串)
Cat.names==["Iris","Andy"]

// @since 1.1.30
"基本字符串"=="基本字符串"
${company}=="巨星行动电子商务优先公司"

// @since 1.1.33
// 可以使用Bean的多级属性求值的方式进行断言,注意不支持Map的操作,只支持JavaBean。若属性名不存在的话,则会直接抛出异常。
User.name=="于金平"
User.company.name=="巨星行动"
User[name="林小姐"].company.name=="友邦保险"
User[name="没有这个用户"].size()==0
User[name="没有这个用户"].company=="null"
User[name="没有这个用户"].company.name=="null"

// @since 1.1.38
// 可以使用Bean的list属性进行过滤并求值。若过滤条件找不到任何符合条件的值,则返回null值。使用如下演示数据
// 部门数据 departments=[{id:1,name:"人事部"},{id:2,name:"后勤部"}]
// 雇员数据 employees=[{id:1,name:"老大"},{id:2,name:"王二"},{id:3,name:"张三"},{id:4,name:"李四"}]
// 注意: 1.1.38 暂不支持多级属性值的方法调用,比如Company.test.departments[id="1"].size()==2不支持
Company.departments[name="后勤部"].employees[name="李四"].id==4L
Company.departments[id="1"].name=="人事部"
Company.departments[id="1"].employees[name="王二"].name=="王二"
Company.departments[id="1"].employees[name="王二"].name==Company.employees[id="2"].name

4.8.4. 自定义变量

当你的 Excel 表中需要动态内容的时候,例如断言今天是几号?等无法在 Excel 表中写入纯静态数据的,支持使用自定义变量的方式将数据注入。一旦你的测试类 添加了自定义变量,那么你就可以在 Excel 表中通过 ${name}的方式来访问变量并进行断言。 @since 1.1.20

  • 添加自定义变量的 Java 代码,只需要将 @VariableInject 注解添加到只有 1 个Variable 类型参数的方法上即可。 例如:

    @VariableInject
    public void inject(Variable variable){
        variable.addVar("company","昆山巨星行动电子商务有限公司");
        variable.addVar("today", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
        variable.addVar("date", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)+" 00:00:00");

        // 1.1.43 版本新增允许加入 JavaBean 对象,例如:User对象
        variable.addVar("user",new User());
    }
  • 在各个 Excel 表中使用`${company}`、${today}`和${date}`即可输出该变量内容。对于加入的JavaBean, 你可以用属性路径的方式引用Bean的属性值 例如: ${user.address.province.name} ,用来获取 User>>Address>>Province>>name这个属性值。

  • 注意事项: ${name} 书写时遵循 Java 变量规范,${`和}`之间不允许出现空格。 ${name} 将原样输出自定义的内容,如果你需要的是字符串,那么应该在 Excel 表中将该变量引用${name}使用英文双引号包裹,即"${name}"的方式。

4.9. Mock表达式语法参考

对于 mock 表达式,在 Excel 文件表 MockData 中分为四列,分别是:MockMethod,Parameters,ReturnValue,ReturnType。 下文中说明各个语法格式的时候,会用[]的方式包裹格式,其中[]内的内容是必须的。而[]本身并不是格式的一部分,而是用来说明格式的。

4.9.1. MockMethod

格式:[属性名.方法名]的形式。属性名是测试类中定义的属性名,方法名则是该属性的方法名。 例如:stockLogisticsItem.getPrice 那么 stockLogisticsItem 是属性名,getPrice 是方法名。

4.9.2. Parameters

格式:[参数值,参数值]的形式。参数值的位置应与 MockMethod 方法声明的参数的位置一致。 参数值的格式支持如下几种类型:

  1. 如果参数是基本数据类型,则参数值直接使用,例如:123,"abc" 等

  2. 如果参数是复杂类型,则参数值使用 JSON 格式,例如:{"name":"测试商品","sku":"SKU100"}

  3. 如果参数是集合类型,则参数值使用 JSON 格式,例如:[{"name":"测试商品","sku":"SKU100"},{"name":"测试商品","sku":"SKU100"}]

  4. 如果参数是集合类型,且参数值是基本数据类型,则使用 JSON 格式,例如:[123,456,"商品名称"]

  5. 如果参数是 JavaClass,则使用 class:: + `Java 类全名`的形式,例如:class::com.example.domain.StockLogisticsItem 表示该参数是一个 JavaClass 类型,且该参数是一个 StockLogisticsItem 类型。 @since 1.1.26

4.9.3. ReturnValue

ReturnValue 是 MockMethod 的返回值。 通常情况下,ReturnValue 是基本数据类型,或者复杂类型,或者集合类型。

  1. 如果 ReturnValue 是基本数据类型,则直接使用,例如:123

  2. 如果 ReturnValue 是复杂类型,则使用 JSON 或 JSONArray 格式,例如:{"name":"测试商品","sku":"SKU100"}

对于返回值的特殊模式当前支持如下几种类型:

  1. ref:: 表示引用其他 Mock 属性值,例如:ref::stockLogisticsItem 表示引用测试类中名称是 stockLogisticsItem 这个属性的值。@since 1.1.26

  2. throw:: 表示抛出异常,例如:throw::{"code":123,"msg":"测试异常"} 当返回值是 throw:: 模式的时候,需要在 ReturnType 列标出要抛出异常的类型。@since 1.1.27

4.9.4. ReturnType

ReturnType 是 MockMethod 的返回值类型,但是在 throw:: 语法的情况下,ReturnType 则是要抛出异常的类型。

  1. 如果 ReturnType 是基本数据类型,则直接使用,例如:java.lang.Integer

  2. 如果 ReturnType 是复杂类型,则使用 Java 类全名,例如:com.example.domain.StockLogisticsItem

通常情况下,ReturnType 不是必须的,框架会自动根据 MockMethod 的返回值类型来进行推断。但是,假如你需要模拟该方法抛出异常的时候,ReturnType 是必须填写的,且填写的是抛出异常的类型。

对于多层嵌套的复杂泛型返回类型而言,不要填写该列,请使用 @BeforeMock 注解 的代码方式进行自行指定。

4.10. 模板变量语法参考 (@since 1.1.44)

对于Excel内各个表而言,支持形如 ${变量名称}${实体类[属性名=过滤值].属性名} 的语法。 对于 Excel 内的 TestData 各个列的值,框架默认存在一个名为 params 的内置变量,可以在其他表内引用该变量内的属性值。 此外,${…​} 的方式,除了支持变量名称,还支持数据库表达式。

重点也就是说 ${} 内包裹的要么是 变量 ,要么是 数据库表达式

举例说明如下:

@Slf4j
@ExcelConfiguration
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExtendWith(JTFExtension.class)
public class ExpressTest extends BaseTest{
    @VariableInject
    public void inject(Variable variable){
        variable.addVar("company","昆山巨星行动电子商务有限公司");
        variable.addVar("today", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
    }
    // 或者在测试方法内
    @TestTemplate
    public void testIt(Map<String,String> map){
        // Validator
        String orderNo="从业务系统获取的订单号";
        validator.addVar("orderNo",orderNo);
        // 从版本 1.143 开始允许简单变量的值是JavaBean,并且可以在 Excel 表中使用属性路径的方式应用该变量的属性值。
        validator.addVar("user",new User());

        validator.validate();
    }
}

模板变量语法举例

${params.id} // 表示获取 TestData 表内 id 列的值

${params.province.name} // 表示获取TestData表内province列的JSON对象的name值

${orderNo} // 表示获取简单变量 orderNo的值

${User[id=1].name} // 表示获取实体类User的数据库表中id=1的记录,并获取其属性名name所代表的的字段的值

${User[companyId=Company[name=="巨星行动"]].name} // 表示根据嵌入的过滤条件获取对应的User的name值


// 可以使用内置函数
${Order[id=1].size()} // 获取List列表的长度

${Order[id=1].sum(amount)} // 计算List列表内所有订单金额之和

${Order[id=1].json()} // 表示获取 Order 表中 id=1的数据对象并用json的方式输出,此时输出的是JsonArray对象文本

${Order[id=1].first().json()} //  表示获取 Order 表中 id=1的数据对象并取第一个元素用json的方式输出,此时输出的是Json对象文本

4.11. 框架内置函数

在断言语法、Mock语法以及模板变量语法 中,暂时支持如下内置函数:

  1. size() 用于获取List列表的数据数量。

  2. sum(属性名) 用于获取List列表数据的某字段的合计值。

  3. first() 用于获取List列表的第一个元素。

  4. json() 用于输出对象的json形式的文本值。

具体使用参见上述 模板变量语法举例

5. 疑难杂症问答

5.1. 各项目通用

5.1.1. 如何让 Mock 方法返回一个已知的 mock 对象

需求如下:

如下代码,希望在Excel中定义调用 springContextUtils.getBean(JackyunDeliveryService.class)的时候直接返回 jackyunDeliveryService 这个已经被 Mock 好的对象。

    @Mock
    private JackyunDeliveryService jackyunDeliveryService = new JackyunDeliveryServiceImpl();
    // 类型:com.dmj.util.SpringContextUtils
    @Mock
    private static SpringContextUtils springContextUtils = new SpringContextUtils();

解决方法(@since 1.1.26):

  1. 首先需要使用 jtf 框架版本>=1.1.26提供的 jar 文件。

  2. 在 Excel 表 MockData 中添加如下Mock配置(列之间用 | 标识)

id MockMethod Parameters ReturnValue ReturnType

1

springContextUtils.getBean

class::com.dmj.seckill.service.service.JackyunDeliveryService

ref::jackyunDeliveryService

 

5.1.2. 如何模拟(mock)一个静态类/静态方法

解决方法(@since 1.1.21):

使用如下形式进行静态类和静态方法的 mock(实例化该静态类后与普通的属性一样 mock),并在 Excel 中配置 mock 返回值等信息即可。

    @Mock
    private static StringUtils stringUtils = new StringUtils();

5.1.3. 如何 mock 一个方法抛出指定的异常(Exception)

解决方法(@since 1.1.27):

在 Excel 表 MockData 中添加如下Mock配置,该配置将模拟 jackyunDeliveryService.getDeliveryInfo 方法抛出 com.dmj.exposed.api.bean.exception.BizException 异常。

id MockMethod Parameters ReturnValue ReturnType

1

jackyunDeliveryService.getDeliveryInfo

123

throw::{"message":"测试异常"}

com.dmj.exposed.api.bean.exception.BizException;

5.1.4. 如何添加断言自定义变量

问题现象/需求:

在测试运行期间,可能某些业务在自动生成业务数据的过程中,通过输入参数及各种过滤条件的方式定位输出断言数据可能比较困难。那么此时,你可以通过用程序代码取回已生成的关键字段数据,作为自定义变量的方式加入断言表达式组。这样断言可能会更加的简洁明了。

解决方法(@since 1.1.30):

    // Validator
    String orderNo="从业务系统获取的订单号";
    validator.addVar("orderNo",orderNo);
    // 从版本 1.143 开始允许简单变量的值是JavaBean,并且可以在 Excel 表中使用属性路径的方式应用该变量的属性值。
    validator.addVar("user",new User());

    validator.validate();

    // Excel断言可以使用如下类似形式的断言
    // ${orderNo}=="从业务系统获取的订单号"
    // MockOrder[orderNo="${orderNo}"].orderNo=="从业务系统获取的订单号"
    // "${user.address.province.name}"=="广东省"

5.1.5. 如何使用代码Mock自动生成UUID的静态方法

问题现象/需求:

对于采用UUID方式生成数据库表ID的,或者自增长的序列的值,由于是未知的,因此无法作为断言数据?如何将生成的值变为已知的,尽管每次获取的值是不同的,但是却是已知的呢?这样的话,我们就可以用在断言里了! 由于当前版本jtf暂时无法支持在Excel里写动态的迭代(iterator)的结果,因此需要使用Java代码的方式辅助完成。这类的方法通常情况下是静态的。

解决方法:

假如我们有个IdUtil类有如下 uuid()静态方法,我们希望每次调用都会返回不同的结果,尽管是不同的结果,但是希望每次调用返回的是我们已知的或可预期的值。 如下是该方法的简单实现:

    public static String uuid(){
        return UUID.randomUUID().toString();
    }

下面在 MockTest.java 中实现对该方法的mock,我们希望的结果是: 第1次调用uuid()方法返回"value1",第2次调用该方法返回"value2",第3次调用该方法时返回"value3"。 以此类推即可。

@Slf4j
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExcelConfiguration
@ExtendWith(JTFExtension.class)
@DisplayName("Mock测试")
public class MockTest extends BaseTest {
    // 定义 IdUtil的静态mock实例
    private static MockedStatic<IdUtil> mockedStatic;
    // 定义数据索引
    private static AtomicInteger callCount=new AtomicInteger(0);
    // 定义已知要提供的数据
    private static String[] uuids = new String[]{"value1","value2","value3"};
    @BeforeAll
    public static void setup(){
        mockedStatic = Mockito.mockStatic(IdUtil.class);
        mockedStatic.when(()->IdUtil.uuid()).then((Answer<String>) invocation -> uuids[callCount.getAndAdd(1)]);
    }
    @TestTemplate
    void testMock(Map<String,String> map){
        callCount.set(0); //重设索引值
        Assertions.assertEquals("value1",IdUtil.uuid()); // 断言第1次获取的的是 value1
        Assertions.assertEquals("value2",IdUtil.uuid()); // 断言第2次获取的的是 value2
        Assertions.assertEquals("value3",IdUtil.uuid()); // 断言第3次获取的的是 value3
    }
}

5.1.6. 如何明确指定Mock哪个重载/同名方法 (@since 1.1.34):

问题现象/需求:

当你要Mock的类中有多个同名且参数数量相同的方法时,jtf框架默认是按照2个条件去查找你要Mock的方法/函数

  1. 方法/函数 的名称与Excel中声明的名称相同

  2. 方法/函数 拥有与Excel中参数值的个数相同的参数数量

然而这2个条件依旧无法确定唯一一个方法的时候,就需要明确指出要调用哪个方法。 假设TestUtil.java中有如下两个方法:

    public static String format(Integer i,String format){
        return String.format("%d",i);
    }

    public static String format(Long i,String format){
        return String.format("%dL",i);
    }

我们需要Mock的是第2个带有Long类型参数的format方法。

解决方法:

  1. 在Excel的MockData表的最右侧添加一列英文名是"ParameterTypes",中文名是"参数类型" 的列。

  2. 按照该方法/函数的参数签名,将各个参数的类型全名写入到该列,各个参数类型之间使用英文逗号分割开来即可。 参考如下:

java.lang.Long,java.lang.String

若Excel的MockData表无`ParameterTypes`列或该列的对应值是空的,则仍旧会默认mock找到的同名方法中的第1个。

5.1.7. 使用DbExpect时如何要系统自动识别数据库实体类 (@since 1.1.34):

问题现象/需求:

当你使用 DbExpect 进行数据库断言的时候,系统会自动扫描数据库实体类,并根据配置的注解帮你解析成为数据库中对应的表与字段。 但是,因为通常情况下,你使用的是Class的sampleName(为了书写简便),而不是带有package的类全名,那么则可能会导致无法找到该Class。 当无法找到该Class的时候,DbExpect操作就可能会抛出空指针异常NullPointException。

解决方法: 解决这个问题,你需要在配置文件[/META-INF/services/top.yjp.jtf.core.junit.InitFactory.properties]文件中增加如下配置:

# 对于使用 DbExpect 列进行断言的时候,如果出现找不到 数据库实体类Class 的时候,请加入如下配置
ScanClassInitFactory =top.yjp.jtf.core.junit.factory.ScanClassInitFactory

该配置将会自动扫描你的项目中的Class。

注意 鉴于各个项目的架构不同 ,@since 1.1.34 增加了对maven项目的特别处理。因此,若你的项目是maven标准项目,请升级到 1.1.34 版本及以上。

5.1.8. 如何使得已经mock的静态方法在线程中也能正确生效 (@since 1.1.36):

问题现象/需求:

当我们使用了Mockito的静态方法mock时,通常情况下其mock只会在当前线程中生效。原因就是如果不限制的话,若多线程运行JUnit,则可能不同的测试用例都有不同的mock需求。此时会导致意料之外的结果,且不确定。 因此,通常情况下 mock都只是在当前线程生效。对于一些使用 CompletableFuture.runAsync 的方式运行异步线程的,其mock自然会失效。由于我们的项目通常不会进行多线程方式运行单元测试,因此基于这个前提,有了如下的解决方案。

解决方法(@since 1.1.36):

  1. 首先升级到 jtf 测试框架 1.1.36及以上的版本

  2. 在你的 test/resources/META-INF/services/top.yjp.jtf.core.junit.InitFactory.properties 文件中添加如下行

# 若你需要在线程中使已Mock的静态方法生效,请加入如下配置
MockInitFactory = top.yjp.jtf.core.junit.factory.MockInitFactory

若你的项目异步线程调用不是用的 CompletableFuture.runAsync 方法,可以提出需求,我将扩充这个 MockInitFactory 的具体实现,届时你只要升级框架版本即可。

5.1.9. 如何确保要Mock的方法返回值对象类型ReturnType的准确性 (@since 1.1.36):

问题现象/需求: 由于Java的泛型使用的是擦除的方式,因此某些要Mock的方法返回值对象中若有多层的泛型,则可能Mock后返回的值类型不正确。例如 List<Order> 可能会变成 List<JSONArray>。

解决方法(@since 1.1.36): 请将要返回的Mock类型用如下的方式,传递给jtf框架:

// @BeforeMock 注解的方法,将在Mock各个对象之前运行
@BeforeMock
public void beforeMock(Mocker mocker){
    // setMockMethodReturnType 有2个参数,用来设置 mock 表达式的返回类型
    // 第一个参数是 mock 表达式的字符串
    // 第二个参数是该 mock 表达式的返回类型
    mocker.setMockMethodReturnType("skuProductService.queryItemProductQuantity", new TypeReference<Map<Long, Map<String, ProductDto>>>() {});
    mocker.setMockMethodReturnType("stargoUtil.sendCloudDelivery2", new TypeReference<HxDeliveryResp<HxDeliveryData>>() {});
}

5.1.10. 如何在单元测试中捕获调用第三方SDK方法时传递的参数值并验证参数传递是否符合预期 (@since 1.1.37):

问题现象/需求:

在我们与第三方系统或SDK进行交互的时候,最佳实践是通过自己项目的代码将第三方的SDK再次封装,然后使用Mock方式对已封装的类进行Mock。通常我们假定第三方返回正常,也就是按照你Mock的预期结果返回。 在这个过程中,我们需要判断我们传递给第三方SDK的参数是否符合我们的预期,如何做呢?

解决方法(@since 1.1.37): 请看如下示例代码及相应的注释

// 此处我们假设在单元测试代码里需要 Mock 阿里支付的SDK
@Mock
private AlipayBaseService alipayBaseService;

public void doTest(Map<String,String> params){
    // 首先声明,你需要捕获的参数的类,本例中是 AlipayMerchantOrderSyncRequest
    ArgumentCaptor<AlipayMerchantOrderSyncRequest> captor = ArgumentCaptor.forClass(AlipayMerchantOrderSyncRequest.class);

    // 调用你的业务方法,该方法中调用了阿里支付的SDK
    alipayBaseService.merchantOrderSync(payOrder, notifyStatus);

    // 校验 alipayBaseService 这个mock对象中,执行了1次(具体几次视你的业务而定)merchantOrderSync方法,并捕获这个方法的参数到 captor,这个步骤必须要有verify
    // 若你需要捕获多个参数值,则需要定义多个 ArgumentCaptor 与函数参数一一对应,并且如下代码也需要增加对应的 *.capture()的方法调用。
    Mockito.verify(alipayBaseService, Mockito.times(1)).merchantOrderSync(captor.capture());
    // 将捕获的参数值取出放到内存变量
    AlipayMerchantOrderSyncRequest orderEvent = captor.getValue();
    // 将请求参数加入到内存变量验证队列,并在Excel文件中撰写断言即可
    validator.addSingleObject(orderEvent);
}

5.1.11. 如何在单元测试环境使用单元测试专用的redis数据库 (@since 1.1.41):

问题现象/需求:

在我们项目中可能会存在使用redis作为缓存或锁的时候,由于Redis属于单独存在的服务,对于重复运行的JUnitTest需要不断的重置Redis的环境。因此需要测试框架提供一些工具,帮助测试人员进行Redis的测试操作。如何做呢?

解决方法(@since 1.1.41):

首先在你的测试项目中将 jtf单元测试框架 升级到 1.1.41及以上版本。然后按照如下方式配置你的项目:

  1. @EnableRedis:在你的单元测试类上使用 @EnableRedis 自定义注解,注解与 @ExcelConfiguration 一样配置在 class 级别上

  2. 在你的测试基类中增加,protected RedisOperator redisOperator; 属性,并使用 @ExcelInject 注解标注

  3. 在你的测试基类中增加,返回值类型为 RedisDbConfig 的方法,返回在 /redis-db-config.properties 配置文件中的配置,当然你也可以不用配置文件,直接代码中硬编码这个配置。

如下列出 BaseTest.java 增加Redis支持的内容,大致如下:

public class BaseTest {

    @ExcelInject
    protected RedisOperator redisOperator;

    @ExcelInject
    public RedisDbConfig createRedisDbConfig(){
        // 此处使用的是默认读取 classpath://redis-db-config.properties 配置,你也可以硬编码实现 或者直接读取 SpringBoot的 redis 配置信息
        DefaultRedisDbConfigProvider provider = new DefaultRedisDbConfigProvider();
        return provider.getRedisDbConfig();
    }
}

redis-db-config.properties 文件内容如下所示【此配置为j-youxuan.com域名相关项目 @夏耀明 项目组】:

redis.host=10.30.10.225
redis.port=6379
redis.password=gd34O1NT0nqibqv1fo
# 注意:最后的键值格式 TEST_项目代号_时间键值,中间是用英文下划线分割的,前缀不包含格式中的第2个下划线,所以最终实际前缀是 TEST_JTF_
redis.keyPrefix=TEST_JTF

redis-db-config.properties 文件内容如下所示【此配置为hugestargo.com域名相关项目 @宋猛 @冯文亨 项目组】:

redis.host=10.10.10.217
redis.port=6379
redis.password=eien39ynu1UTIi19t3buvq
# 注意:最后的键值格式 TEST_项目代号_时间键值,中间是用英文下划线分割的,前缀不包含格式中的第2个下划线,所以最终实际前缀是 TEST_JTF_
redis.keyPrefix=TEST_JTF

RedisTest.java 文件内容如下所示:

package top.yjp.jtf.core.suite.redis;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import top.yjp.jtf.core.BaseTest;
import top.yjp.jtf.core.annotation.EnableRedis;
import top.yjp.jtf.core.annotation.ExcelConfiguration;
import top.yjp.jtf.core.junit.extension.JTFExtension;

import java.util.Map;

@Slf4j
@EnableRedis
@ExcelConfiguration
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExtendWith(JTFExtension.class)
public class RedisTest extends BaseTest {

    @BeforeEach
    public void beforeEach(){
        // 你可以在 @BeforeEach 方法内对 Redis执行写操作,jtf框架会在你写操作前清除该Redis库中所有带有已配置前缀的KEY值
        for(int i=3000;i<3100;i++){
            String key = String.valueOf(i);
            // 注意: 若你使用 redisOperator 则set方法其键不包括配置的前缀,若你使用SpringBoot的RedisTemplate 或 自定义的RedisUtil 则需要使用带有前缀的KEY
            redisOperator.set(key,String.valueOf(i));
        }
    }
    @TestTemplate
    public void testRedis(Map<String,String> map){
        for(int i=3000;i<3100;i++){
            String key = String.valueOf(i);
            String dbValue =redisOperator.get(key);
            // 注意: 若你使用 redisOperator 则get方法其键不包括配置的前缀,若你使用SpringBoot的RedisTemplate 或 自定义的RedisUtil 则需要使用带有前缀的KEY
            Assertions.assertEquals(dbValue,String.valueOf(i));
        }
    }
}

Redis 测试数据 截图

5.1.12. 如何在Excel内引用表【TestData】内的参数值 (@since 1.1.43):

问题现象: 对于单元测试框架jtf内的Excel文件,如何在MockData、DbData、DefaultObjectData这3张表内引用已经定义的TestData内的输入参数的值呢? 例如 在 TestData 定义一个字段 user (也就是名称为 user 的列) 其JSON如下

{
  "name": "于先生",
  "address": {
    "location": "国际采购中心",
    "province": {
        "name": "广东省"
    },
    "city": {
        "name": "广州市"
    }
  }
}

那么如何在各个Excel表内引用这个JSON内的值呢?

解决方案:

TestData 表内的所有字段都存在一个特殊的简单变量中 ${params},引用格式 ${params.列名.json路径值},比如上述JSON中我想获取地址中的省份 user.address.province.name, 这样写 ${params.user.address.province.name} 即可。如果 address 这个对象是 null 值的话,则整个表达式也会返回值 null。

如果你的参数列只是Java基本类型,如:String,Boolean,Integer,Long,Double 等,假设你的列名是 orderNo , 那你直接使用 ${params.orderNo} 即可获取其值。

5.1.13. 如何快速生成JSON和H2SQL建表语句 (@since 1.1.45):

问题现象:

对于测试而言,比较耗时的是测试数据的构造,同时由于jtf采用在Excel文件中嵌入JSON格式数据的方式,会出现手工构造JSON数据的耗时情况。同时,各个项目采用的是MYSQL,导致拷贝建表语句后,为了兼容H2数据库,需要不断修改语句直至符合H2兼容。 针对这2种现象,可采用如下建议方案。

解决方案:

  1. 升级jtf到 1.1.45 版本及以上

  2. 使用如下Java代码(将包名改成你自己项目的包名即可)生成并打印到控制台后复制粘贴即可

package top.yjp.jtf.core.tools;

public class JtfTools {
    /**
     * 生成指定表建表语句,忽略掉所有索引、外键等
     * @param tableName 表名
     */
    public static void m2h(String tableName) {
        MySqlH2 m2h = new MySqlH2();
        m2h.setUrl("jdbc:mysql://10.30.10.213:3306/jxyx_shop");
        m2h.setUsername("mdjkjxyx");
        m2h.setPassword("tiyn36urvVQV5");
        m2h.setDatabaseName("jxyx_shop");
        // 若你的工程需要独特的类型替换等操作,请打开如下注释代码
        // 文本文件格式 每一行用=号分割,左边是旧字符串,右边是新字符串
        // 如果是放在classpath下,则请用classpath:开头
        m2h.setReplaceFile("classpath:MySqlH2.txt");

        m2h.printTable(tableName);
    }

    public static void main(String[] args) {
        // 是否是打印数据库表
        boolean printTable = false;

        // 打印JSON时是否使用格式化功能
        boolean prettyFormat = false;

        String tableName = "app_user_md";
        String className = "top.yjp.ext.spring.suite.express.MockExpressionTestBean";
        if (printTable) {
            m2h(tableName);
        } else{
            EntityToJson.printJson(className,prettyFormat);
        }
    }
}

5.2. SpringBoot 项目

5.2.1. MybatisPlus 的 Mapper在Mock后使用LambdaQueryWrapper 报错抛出 MybatisPlusException

问题现象:

SpringBoot 项目使用 @Mock 注解成功 Mock 了一个 MybatisPlus的 Mapper后,如果使用该 Mapper 的LambdaQueryWrapper的形式进行数据库查询,则会报如下错误

MybatisPlusException: can not find lambda cache for this property [****]

解决方法(@since 1.1.25):

  1. 首先需要使用 jtf 框架版本>=1.1.25提供的 jar 文件。

  2. 在配置文件 src/test/resources/META-INF/services/top.yjp.jtf.core.junit.factory.BeanFactory.properties 中添加如下配置

# 要求 jtf 框架升级到 >=1.1.25 版本
# 解决 MybatisPlus的 Mapper在 Mock 后抛出 MybatisPlusException 的Bug. 在配置文件最后添加如下配置
top.yjp.jtf.core.junit.MockHook=top.yjp.jtf.ext.spring.mock.MybatisPlusMockHook

5.2.2. MyBatisPlus的Java枚举类型转换为数据库兼容字段值

问题现象:

SpringBoot项目的数据库实体类使用了MyBatisPlus的注解 @EnumValue (Enum类型)后,无法正确转换为存储到H2数据库的值。会报 CAST(X ) 一堆的错误。原因是 Enum 需要转换为H2支持的字符串方式。 例如:

解决方法:(@since 1.1.47):

  1. 升级jtf版本到 1.1.47 及以上

  2. 在SpringBoot项目的配置文件 top.yjp.jtf.core.junit.InitFactory.properties 中加入如下配置即可。

# 要求 jtf 框架升级到 >=1.1.47 版本
# 如果你在SpringBoot项目中的数据库实体类使用了Enum类型,请将如下配置增加到 top.yjp.jtf.core.junit.InitFactory.properties 中
# 增加后将会对Enum类型进行值转换
SpringSqlParameterValueInitFactory=top.yjp.jtf.ext.spring.factory.SpringSqlParameterValueInitFactory

5.3. Jodd 项目

5.3.1. Jodd项目使用JDateTime属性字段,实体类指定了表名后无法生成正确的 INSERT SQL 语句问题

问题现象:

Jodd 项目使用 @DbTable 注解(该注解未设置任何参数)时,Jodd写入数据库操作成功;当@DbTable("table_name")指定一个表名的情况下,生成的INSERT SQL语句中,使用了JDateTime类型的属性的值并没有被单引号包裹, 导致 INSERT SQL 语句语法错误(即生成了类似如下的错误SQL)。

-- 实际上 create_time是实体类中的JDateTime类型,其值转换为INSERT SQL 缺少单引号包裹,故而生成了如下错误的SQL语句
INSERT INTO table_name(id,username,create_time)values (1,'admin',2024-07-29 20:50:00);

解决方法:(@since 1.1.41):

  1. 首先修改测试环境下的H2数据库连接,在URL中添加如下属性 DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE

  2. 其次修改测试环境下的 app.props 修改`dboom`配置 dboom.tableNames.uppercase=falsedboom.columnNames.uppercase=false

这样需要测试环境与生产环境的配置文件是分开的才行,否则可能需要用代码动态调整的 dboom.tableNames.uppercase=falsedboom.columnNames.uppercase=false 的值以匹配生产环境MYSQL和单元测试环境H2。

5.3.2. 如何在Jodd项目单元测试类中继续使用 @PetiteInject 注解

问题现象:

Jodd 项目通常使用 @PetiteInject 注解启用Bean的容器注入功能,但Jodd测试类可能是全新的包,与原有的Service等包名完全不同,根据Jodd的规则,此时测试类内使用 @PetiteInject 注解将无效(即无法进行容器注入)。 既然使用注解 @PetiteInject 的书写方式更符合该项目研发人员的思维方式及代码书写习惯,那么如何使得 @PetiteInject 能够在单元测试类上生效呢?。

解决方法:(@since 1.1.42):

  1. 超级简单,只要将jtf测试框架升级到版本>=1.1.42 ,Jodd项目即可自动拥有该项功能,此时你就可以愉快的在Jodd项目的测试类中使用 @PetiteInject 注入 Jodd 中使用的 @PetiteBean 标注的Bean了。

5.3.3. 在Jodd项目单元测试类中使用 @Mock 注解的注意事项

问题现象:

Jodd 项目通常使用自有的 @PetiteInject@PetiteBean 这两个注解启用Bean的容器注入功能。但是Jodd项目并没有 SpringBoot@MockBean 这样的注解。 因此,即便是单元测试类已经Mock了一个Bean的实例,但这个实例Bean却不是Jodd容器内的实例Bean,因此对于Jodd项目来说,需要在测试类代码中额外做一点事情。

解决方法:(@since 1.1.42):

举例如下:

UserOrderService,WxOrderService,WxSdkService 这3个服务类。

UserOrderService 注入了 WxOrderService 的实例 wxOrderService,而 WxOrderService 又注入了 WxSdkService 的实例 wxSdkService

形成了如下的调用链关系 userOrderService.wxOrderService.wxSdkService

我们需要Mock wxSdkService 这个属性Bean,从而达到隔离微信支付等第三方系统的调用(即测试过程中不调用第三方系统,直接返回我们想要的结果)。

示例代码如下,并重点关注其中的中文注释。

import org.mockito.Mock;
import top.yjp.jtf.ext.jodd.util.JoddReflectUtil;

@DisplayName("微信订单取消单元测试")
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ExcelConfiguration
@ExtendWith(JTFExtension.class)
public class OrderCancelJunit extends BaseJunitTest {
    // 将 WxSdkService 类标记为 mock
    @Mock
    WxSdkService wxSdkService;

    // 注入 Jodd Bean
    @PetiteInject
    UserOrderService userOrderService;

    @DisplayName("微信订单取消单元测试")
    @TestTemplate
    public void cancelOrder(Map<String, String> map) {
        log.info("MAP={}",map);
        String userId=map.get("userId");
        String orderId=map.get("orderId");


        //注意:Jodd mock 需要增加如下一行代码的替换操作
        // 使用已mock的 wxSdkService 替换掉 jodd 注入的wxSdkService,此方法必须在业务方法执行之前调用。
        JoddReflectUtil.setPropertyValue(userOrderService, "wxOrderService.wxSdkService", wxSdkService);


        //
        // 假设我们测试的业务是用户userId的order订单进行微信退款操作,执行业务处理
        RefundResult result = userOrderService.refund(userId,orderId);


        validator.addSingleObject(result);
        validator.validate();
    }
}

只是额外增加了一行代码 JoddReflectUtil.setPropertyValue(userOrderService, "wxOrderService.wxSdkService", wxSdkService); 使用已经Mock好的 wxSdkServiceuserOrderService 里间接引用的 wxSdkService 替换掉即可。