Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340
使用CI/CD可以实现按照要求自动执行单元测试,例如定期执行某个项目的单元测试。当在CI服务器执行单元测试时,CI服务器有可能无法访问数据库服务器。可以使用H2数据库,使执行单元测试时不依赖数据库服务器。
使用H2数据库执行单元测试的其他优势是,不会在数据库服务器中产生脏数据。
以下说明在单元测试中如何在本地使用H2数据库,摆脱对数据库服务器的依赖,帮助单元测试执行支持自动化。
参考 http://www.h2database.com/html/main.html
H2是一个开源的Java实现的SQL数据库,支持嵌入模式与服务器模式,支持内存数据库,提供了JDBC API。
参考 http://www.h2database.com/html/quickstart.html
以嵌入模式在应用中H2数据库,可以通过以下方式使用:
参考 http://www.h2database.com/html/features.html#compatibility
所有数据库引擎的行为都有些不同。在可能的情况下,H2支持ANSI SQL标准,并尝试与其他数据库兼容。但是仍然存在一些差异:
在MySQL中,文本列默认不区分大小写,而在H2中,它们区分大小写。H2也可以支持列不区分大小写。
对于某些功能,H2数据库可以模拟特定数据库的行为。但是,这种方式仅实现了数据库之间差异的一小部分。
参考上述文档“MySQL Compatibility Mode”部分。
当需要使用MySQL模式时,可使用类似jdbc:h2:~/test;MODE=MySQL;DATABASE_TO_LOWER=TRUE
的数据库URL,这种模式也能兼容MariaDB。当需要添加区分大小写的标识符时,可在URL添加;CASE_INSENSITIVE_IDENTIFIERS=TRUE
。在创建数据库后不要修改DATABASE_TO_LOWER
参数值。
参考 http://h2database.com/html/features.html#database_url
在单元测试中使用H2数据库时,可能会使用其支持的嵌入(本地)模式连接、内存数据库、在连接时执行SQL等。
参考 http://www.h2database.com/html/features.html#embedded_databases
使用嵌入(本地)模式连接时,会在本地生成数据库文件。连接本地数据库的URL及示例如下所示:
jdbc:h2:[file:][]
jdbc:h2:~/test
jdbc:h2:file:/data/sample
jdbc:h2:file:C:/data/sample (仅在Windows环境中使用)
前缀“file:”是可选的。如果不使用路径或仅使用相对路径,则将当前工作目录用作起点。路径和数据库名称是否区分大小写取决于操作系统,建议仅使用小写字母。数据库名称必须至少包含三个字符(File.createTempFile的限制)。数据库名称不得包含分号。使用/可以指向用户主目录,例如:“jdbc:h2:/test”。
使用H2数据库嵌入(本地)模式时,在测试结束后可对数据库数据进行检查。
参考 http://www.h2database.com/html/features.html#in_memory_databases
连接内存数据库的URL及示例如下所示:
jdbc:h2:mem:(私有模式)
jdbc:h2:mem:(命名模式)
jdbc:h2:mem:test_mem(命名模式)
某些场景并不需要持久保存数据,或不需要持久保存数据的修改。这种情况下可以使用H2数据库的内存模式,数据不会被持久保存。
某些情况下,仅需要一个内存数据库的连接。这意味着要打开的数据库是私有的。在这种情况下,数据库URL为“jdbc:h2:mem:”。在同一虚拟机中打开两个连接意味着打开两个不同的数据库。
有时需要对同一个内存数据库的多个连接。在这种情况下,数据库URL必须包含一个名称。例如“jdbc:h2:mem:db1”。使用此URL访问相同的数据库仅在相同的虚拟机和类加载器环境中有效。
参考 http://www.h2database.com/html/features.html#execute_sql_on_connection
某些情况下,特别是对于内存数据库,当客户端连接到数据库时,可能需要自动执行DDL或DML命令,可通过INIT属性启用。可以将多个命令传递给INIT,需要使用分号及转义字符作为分隔符,如下所示:
String url = "jdbc:h2:mem:test;INIT=runscript from '~/create.sql'\\;runscript from '~/init.sql'";
双反斜杠仅在Java或properties文件中是必需的。在GUI或XML文件中,只需要一个反斜杠:
<property name="url" value=
"jdbc:h2:mem:test;INIT=create schema if not exists test\;runscript from '~/sql/init.sql'"
/>
当需要在INIT中指定反斜杠\
时,也需要进行转义。假如需要指定Windows的路径,可使用正斜杠替代,会更简单。
参考 http://www.h2database.com/html/commands.html#runscript
RUNSCRIPT命令用于从文件执行SQL脚本。该脚本是一个包含SQL语句的文本文件。每个语句必须以“;”结尾。
可以使用URL代替文件名。若需要从classpath中读取流,可使用前缀’classpath:'。
RUNSCRIPT命令用法为“RUNSCRIPT FROM”,示例如下:
RUNSCRIPT FROM 'backup.sql'
RUNSCRIPT FROM 'classpath:/com/acme/test.sql'
参考 http://www.h2database.com/html/features.html#database_file_layout
当使用默认的MVStore引擎 http://www.h2database.com/html/mvstore.html#overview 时,持久保存的数据库会创建以下文件。
文件名格式 | 文件名示例 | 描述 | 文件数量 |
---|---|---|---|
(database).mv.db | test.mv.db | 数据库文件,包含所有表的事务日志,索引和数据 | 每个数据库1个文件 |
(database).newFile | test.newFile | 用于数据库压缩的临时文件,包含新的MVStore文件 | 每个数据库0个或1个文件 |
(database).tempFile | test.tempFile | 用于数据库压缩的临时文件,包含临时MVStore文件 | 每个数据库0个或1个文件 |
推荐使用DBeaver对H2数据库进行管理
,比H2本身提供的工具及SQuirreL SQL更好用。
可从以下地址下载DBeaver后安装:
https://dbeaver.io/download/
https://github.com/dbeaver/dbeaver/tags
可从本地的Maven/Gradle缓存目录中获取H2数据库驱动对应的jar包。
也可从Maven仓库下载H2数据库驱动,如2.1.214版本对应的地址为:https://repo1.maven.org/maven2/com/h2database/h2/2.1.214/h2-2.1.214.jar
如果需要下载H2数据库驱动的其他版本,可在以下目录中查找:https://repo1.maven.org/maven2/com/h2database/h2/
下载H2数据库驱动jar包后,可在DBeaver安装目录中创建目录(如“drivers”),将H2数据库驱动jar包保存在对应目录。
获取H2数据库驱动之后,需要在DBeaver中配置H2数据库驱动。
点击菜单“数据库”“驱动管理器”;
打开“驱动管理器”窗口后,输入“h2”(不区分大小写),点击“H2 Embedded V.2”选项,点击“编辑”按钮;
打开“编辑驱动”窗口后,点击“添加文件”按钮;
选择以上下载的H2数据库驱动jar包。
在DBeaver中,点击“数据库”“新建数据库连接”菜单;
或者点击以下图标:
打开“连接到数据库”窗口后,点击“嵌入式”标签页,点击“H2 Embedded V.2”,点击“下一步”按钮;
在“路径”右边的输入框输入需要打开的H2数据库文件完整路径,或者点击“浏览”按钮后选择;
以上路径直接使用当前操作系统的文件路径格式即可,不需要增加其他参数
,如下所示:
D:\desktop\h2db\jacg_h2db_rbc.mv.db
指定H2数据库文件路径后,点击“完成”按钮。
在DBeaver中创建H2数据库连接后,会显示在左侧的列表中,点击列表前面的“>”图标可以打开H2数据库文件;
也可在列表中数据库对应项点击右键,点击“连接”菜单打开,还可进行编辑、删除、重命名等操作;
在列表中选中数据库对应项后,以下三个按钮分别可以用于连接数据库、断开/重新连接数据库、断开连接;
在DBeaver中,打开“SQL 编辑器”“SQL 编辑器”菜单,或按“F3”键,可以打开SQL编辑器。
假如弹出了“Choose SQL script for”窗口,可以点击“New scrip”按钮,打开新的SQL编辑器窗口。
H2数据库使用的schema默认为大写,假如在创建H2数据库时指定的schema为小写,则执行SQL语句时,需要指定schema,且schema、表名、字段名需要使用半角双引号包含
,如下所示:
SELECT "col", "col2" FROM "schema"."table_name" WHERE "col3" = '...'
在以上情况下,若使用常规方式的SQL语句,即不指定schema,schema、表名、字段名不使用半角双引号包含,则执行时会出错:
假如未指定schema,则会提示“SQL 错误 : Table “…” not found”;
假如有指定schema但未使用双引号,则会提示“SQL 错误 : Schema “…” not found”。
为了能够对H2数据库执行常规方式的SQL语句,可以按照以下方式处理。
无论以下使用哪种实现方式,都需要先按照当前步骤处理。
在DBeaver左侧连接列表中数据库对应项点击右键,点击“编辑 连接”菜单(前文有截图),或按“F4”键,打开“连接…配置”窗口。
在默认打开的“连接设置”“主要”标签页,修改路径中指定的H2数据库文件路径,将文件后缀".mv.db"删除
例如H2数据库文件路径如下:
D:\desktop\h2db\jacg_h2db_rbc.mv.db
则修改后需要指定如下:
D:\desktop\h2db\jacg_h2db_rbc
以下的“自动切换schema”方式,在打开H2数据库时会自动
切换到指定的schema,可以执行常规方式的SQL语句;
“手动切换schema”方式,在打开H2数据库时不会自动
切换到指定的schema,需要手动切换schema之后,才可以执行常规方式的SQL语句。
在以上打开的“连接…配置”窗口中,点击“编辑驱动设置”按钮,打开“编辑驱动…”窗口;
通过前文所述,点击菜单“数据库”“驱动管理器”,打开“驱动管理器”窗口后,找到H2对应项,点击“编辑”按钮的方式,也可以打开“编辑驱动…”窗口;
在默认打开的“设置”标签页,在“URL 模板”对应的文本框中,在原有的“jdbc:h2:{file}”之后,增加以下内容:
;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;INIT=SET SCHEMA [schema]
以上schema根据实际情况替换为对应的schema名称,例如schema为“jacg”,则完整参数如下:
jdbc:h2:{file};DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;INIT=SET SCHEMA jacg
以上方式是DBeaver打开H2数据库文件时的全局配置,适用于H2数据库文件使用相同schema的场景。假如需要使用DBeaver打开的H2数据库文件的schema各不相同,则以上方式会失效
在“编辑驱动…”窗口“设置”标签页,在“URL 模板”对应的文本框中,在原有的内容之后,增加以下内容:
;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE
在“编辑驱动…”窗口“初始化”标签页,在“默认模式”中选择对应的schema:
以上方式针对DBeaver中特定的H2数据库,需要针对每个H2数据库连接进行配置,适用于H2数据库文件使用不同schema的场景
在“编辑驱动…”窗口“设置”标签页,在“URL 模板”对应的文本框中,在原有的内容之后,增加以下内容:
;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE
在DBeaver左侧连接列表中,点击数据库对应项的“>”图标展开对应的schema列表,在需要切换的schema点击右键,点击“设为默认值”(当前的默认schema会以黑体形式显示)。
在“编辑驱动…”窗口“设置”标签页,在“URL 模板”对应的文本框中,在原有的内容之后,增加以下内容:
;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE
在DBeaver的SQL编辑器中执行以下SQL语句:
SET SCHEMA [schema];
例如schema为“jacg”,则需要执行的SQL语句如下:
SET SCHEMA jacg;
DBeaver也支持通过图形方式对数据库进行操作。
在DBeaver左侧连接列表中,点击数据库对应项的“>”图标展开,可以查看当前数据库中SCHEMA的相关表:
在数据库表对应项点击右键,打开“生成 SQL”菜单,可以选择生成SELECT等SQL语句
例如点击“SELECT”菜单后出现以下SQL语句:
若不钩选“使用完全限定名称”,则显示的sql语句中不会在表名前显示SCHEMA;若钩选则显示的sql语句中会在表名前显示SCHEMA及"."。
点击“复制”按钮可以复制显示的sql语句
可从 http://www.h2database.com/html/download.html 下载H2数据库安装包。
安装后执行bin目录的h2.bat
/h2w.bat
/h2.sh
脚本,会执行h2-xxx.jar中的org.h2.tools.Console类,之后会通过浏览器打开数据库管理界面,可管理H2及其他常见类型的数据库。
启动H2数据库工具后,按照以下方式指定数据库信息并连接:
选择“Generic H2 (Embedded)”。
选择“Generic H2 (Embedded)”。
org.h2.Driver
格式为jdbc:h2:file:[数据库文件路径]
。
数据库文件路径中的后缀“.mv.db”不需要指定
,例如数据库文件路径为E:\UnitTest\build\h2db.mv.db
,则JDBC URL应为jdbc:h2:file:E:\UnitTest\build\h2db
。
不填。
不填。
连接数据库配置如下所示:
连接数据库成功后,如下所示:
参考 http://www.squirrelsql.org/ ,SQuirreL SQL Client是一个Java图形程序,允许查看JDBC兼容数据库的结构,浏览表中的数据,发出SQL命令等,支持多种常见类型的数据库,包括H2。
可从 http://squirrel-sql.sourceforge.net/#installation 下载SQuirreL SQL。
完成安装后,将H2驱动程序h2-xxx.jar拷贝至SQuirreL SQL安装目录的lib目录中。
H2驱动程序h2-xxx.jar可以从Maven中央仓库,或本地Maven/Gradle缓存,或H2安装目录的bin目录获取。
SQuirreL SQL安装目录的lib目录中指定H2驱动jar包版本,需要与生成H2数据库文件时使用的jar包版本一致,否则SQuirreL SQL有可能无法打开对应的H2数据库文件。
执行安装目录的squirrel-sql.bat
/squirrel-sql.sh
脚本,可以启动SQuirreL SQL
启动SQuirreL SQL数据库工具后,打开“Aliases”标签页添加H2数据库信息,与以上使用H2时类似:
Name用于指定数据库别名,不影响使用
连接数据库成功后,如下所示:
打开Objects标签页,再打开数据库对应的schema
由于版本的区别,数据库表对应的菜单名字可能是“BASE TABLE”,而不是“TABLE”
点击连接数据库成功后出现的标签页的X按钮,可以关闭数据库连接
在SQL标签页中,可以输入并执行SQL语句
由于版本的区别,在执行SQL语句时,可能需要在表名前指定schema,并将表名、schema与字段名均使用半角双引号包含,如下所示:
select * from "schema"."table_name" where "column1" = 'xxx' and "column1" <= 35;
在单元测试中使用H2数据库嵌入(本地)模式,执行测试时需要指定数据库建表语句,使H2创建所需的数据库表。在数据库连接URL中通过INIT参数指定建表的SQL脚本文件。
可参考示例项目,读取Groovy配置文件后对数据库连接参数进行了替换,对应unit_test_config.groovy
文件的use_h2_file
元素。
org.h2.Driver
在XML或properties文件中配置时,示例如下:
jdbc:h2:file:./build/h2db;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;INIT=RUNSCRIPT FROM 'classpath:sql/create_table.sql'
在Groovy配置文件中配置时,单引号'
需要进行转义为\'
。
“file:./build/h2db”代表H2数据库文件生成在当前目录的build目录中,文件名为h2db。由于使用了相对路径的形式,“./”不能省略,否则会出现以下异常:
org.h2.jdbc.JdbcSQLNonTransientConnectionException: A file path that is implicitly relative to the current working directory is not allowed in the database URL "...". Use an absolute path, ~/name, ./name, or the baseDir setting instead. [90011-199]
MODE=MySQL;DATABASE_TO_LOWER=TRUE
与CASE_INSENSITIVE_IDENTIFIERS=TRUE
按照文档建议进行配置。
INIT=RUNSCRIPT FROM 'classpath:sql/create_table.sql'
指定了连接数据库时会执行classpath中的sql/create_table.sql
文件,对应示例项目中的src\test\resources\sql\create\_table.sql
文件。
空字符串。
空字符串。
由于每次连接H2数据库时都会执行建表语句,因此需要判断当表不存在时才创建,使用“CREATE TABLE IF NOT EXISTS”。
在H2建表语句的表选项中,不支持ENGINE=InnoDB
、COLLATE=utf8_bin
等选项,支持DEFAULT CHARSET=utf8
、COMMENT
。
在测试类中,在测试方法均执行完毕后,需要关闭数据源,否则在后续的测试类执行数据库操作时可能会提示数据库文件被锁定,异常信息如下所示:
Caused by: org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database may be already in use: null. Possible solutions: close all other connection(s); use the server mode [90020-200]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:622)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:429)
at org.h2.message.DbException.get(DbException.java:194)
...
Caused by: java.lang.IllegalStateException: The file is locked: nio:E:/UnitTest/build/h2db.mv.db [1.4.200/7]
at org.h2.mvstore.DataUtils.newIllegalStateException(DataUtils.java:950)
at org.h2.mvstore.FileStore.open(FileStore.java:166)
at org.h2.mvstore.MVStore.(MVStore.java:381)
...
Caused by: java.nio.channels.OverlappingFileLockException
at sun.nio.ch.SharedFileLockTable.checkList(FileLockTable.java:255)
at sun.nio.ch.SharedFileLockTable.add(FileLockTable.java:152)
at sun.nio.ch.FileChannelImpl.tryLock(FileChannelImpl.java:1108)
at org.h2.store.fs.FileNio.tryLock(FilePathNio.java:121)
...
在示例项目中,测试基类TestMockBase、TestDbBase会在测试方法均执行完毕后关闭数据源(原因可参考前文),若测试类继承该类则不需要进行额外处理。
完成上述操作后,还需要使测试类执行前重新加载Spring Context,否则在使用数据源时可能会提示数据源已关闭,异常信息如下所示:
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.DataSourceClosedException: dataSource already closed at Thu Feb 20 21:41:00 CST 2020
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:26)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:154)
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:141)
...
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Could not get JDBC Connection; nested exception is com.alibaba.druid.pool.DataSourceClosedException: dataSource already closed at Thu Feb 20 21:41:00 CST 2020
at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:80)
at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:81)
at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67)
...
Caused by: com.alibaba.druid.pool.DataSourceClosedException: dataSource already closed at Thu Feb 20 21:41:00 CST 2020
at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1043)
at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:941)
at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:921)
...
在示例项目中,测试基类TestMockBase使用了PowerMock,子类每次执行时都会重新加载Spring Context。
测试基类TestDbBase未使用PowerMock,在测试类级别增加了注解@DirtiesContext(classMode = BEFORE_CLASS)
,使执行该类前重新加载Spring Context。
在单元测试配置参数的数据库连接URL中,需要通过INIT参数指定创建并设置SCHEMA,否则无法使用数据库工具打开生成的数据库文件,会提示“Schema “public” not found”。
在示例项目中,使用H2嵌入(本地)模式时,INIT参数指定的连接时执行的SQL脚本create_table.sql
中,创建并设置SCHEMA为“TEST”,如下所示:
CREATE SCHEMA IF NOT EXISTS TEST;
SET SCHEMA TEST;
在单元测试中使用H2内存数据库模式时,与使用嵌入(本地)模式相比,需要调整数据库连接URL参数
可参考示例项目unit_test_config.groovy
文件的use_h2_mem
元素。
使用内存数据库私有模式的数据库连接URL参数示例如下:
jdbc:h2:mem:;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;INIT=RUNSCRIPT FROM 'classpath:sql/create_table.sql'
需要注意单引号在Groovy配置文件中需要转义。
小数秒精度默认值不同,可参考“JPA自动建表的时间类型”。
H2执行update语句返回的行数,与MySQL使用useAffectedRows=true时的行数相同,即返回受影响行数。
使用H2嵌入(本地)模式时,需要在测试类执行后关闭数据源,并重新加载Spring Context,避免数据库文件被锁定,或获取到已关闭的数据源连接。
对于insert ignore语句,H2与MySQL的执行操作及返回行数均一致。
可参考示例TestDatabaseInsertIgnore类。
插入时若出现错误会被忽略,例如主键或唯一索引出现重复数据。
返回行数为实际执行了插入的数据行数。
对于insert ignore语句,H2与MySQL的执行操作一致,H2与MySQL的jdbc连接参数使用useAffectedRows=true时的返回行数一致。
可参考示例TestDatabaseInsertUpdate类。
当不存在主键或唯一索引相同的数据时,执行插入操作。
当存在主键或唯一索引相同的数据时,执行更新操作。
返回行数如下(MySQL的jdbc连接参数使用useAffectedRows=true时的结果,若为false时返回的行数会不同):
对于replace into语句,H2与MySQL的执行操作一致,返回行数大部分情况一致,存在不同的情况。
可参考示例TestDatabaseReplaceInto类。
当不存在主键或唯一索引相同的数据时,执行插入操作。
当存在主键或唯一索引相同的数据时,先删除旧数据再插入。
返回受影响行数,即删除与插入的数据总和。
H2数据库的replace into语句返回行数与MySQL存在区别,插入存在主键或唯一索引、其他字段均相同的数据,返回对应记录行数*2。
其他情况与MySQL返回行数一致。
SQL语句 | 执行结果 | 返回行数 |
---|---|---|
insert ignore | 相同 | 相同 |
insert on duplicate key update | 相同 | H2与MySQL使用useAffectedRows=true时的行数相同 |
replace into | 相同 | 大部分情况相同 |