Kotlin、JUnit5、Database Rider数据库动态测试实践

来龙去脉

因为项目组一些应用系统需要将Oracle数据库更换为国产分布式数据库,特地基于Kotlin、Junit5、Database Rider等开发了一套可配置的SQL测试工具,以在规模性测试之前,快速了解目标数据库对当前应用中各种SQL的兼容程度和默认行为,从而尽快对目标数据库进行评测,并据此在项目初期对迁移改造工作量有更准确的估计。

由于项目基于Kotlin语言,又用到了JUnit5动态测试特性,以及Database Rider数据库测试框架,特此作一分享,方便需要的同学围观和参考。

当然,数据库厂商或行业肯定有更为专业的工具对数据库的SQL逻辑和兼容性进行测试,至少有SQLsmith、Sqllogictest、SQLancer等,不在本文讨论范围之内,感兴趣的同学可以自行了解,也请专家老师多多批评指正。

JUnit5简介

JUnit 5是JUnit的下一代,于2017年9月首次GA,目标是为JVM上的开发测试建立一个新的基础,包括专注于Java 8及更高版本,以及启用许多不同风格的测试。

JUnit5提供了名为JUnit Platform的可运行不同测试引擎的平台,以及JUnit Jupiter引擎供编写和运行JUnit5风格的测试,还有用于运行JUnit4、JUnit3测试的JUnit Vintage引擎。

JUnit5架构

JUnit5除了提供更为强大的断言工具之外,还提供了嵌套测试、重复测试、参数化测试、模板测试和动态测试等很多强大的测试编写工具,非常的方便。

Database Rider简介

Database Rider形象图

Database Rider是基于数据库进行测试的新工具,旨在使DBUnit更接近您的JUnit测试,让数据库测试更为简单易用。相较于已经不再维护的DbUnit,接棒的Database Rider在测试配置、测试数据集定义、多数据库支持、JUnit5支持、Cucumber支持等很多方面进行了增强。

以下是Database Rider文档中的一个示例:

@RunWith(JUnit4.class)
publicclassDatabaseRiderCoreTest {

@Rule
publicEntityManagerProvideremProvider=EntityManagerProvider.instance("riderDB");

@Rule
publicDBUnitRuledbUnitRule=DBUnitRule.instance(emProvider.connection());


@Test
@DataSet({"users.yml","empty-tweets.yml"})
publicvoidshouldListUsers() {
List<User>users=em().
createQuery("select u from User u").
getResultList();
assertThat(users).
isNotNull().
isNotEmpty().
hasSize(2);
}
}

概览

好了,对相关工具有了简单的了解之后,下面就开始进入正题吧。

首先,我们先看一下最终的运行效果。

Html测试报告

以上就是目前运行完成后生成的Html格式的报告。可以看到目前共有48个测试案例,都运行通过了,耗费17s多。有一点遗憾是,表格中的Method name列显示的内容和预想的不同,暂时还没有找到解决的方法。

下面是在IntellJ IDEA中显示的运行结果。同样地,容器级别未能按照动态测试代码指定的DisplayName去显示。

IntellJ IDEA运行效果

为了实现该测试,使用了如下方式:

设置依赖

首先,我们在build.gradle.kts文件中添加了对Junit5、Database Rider、数据库驱动、Yaml解析、Jexl表达式等的依赖。

dependencies {
testImplementation(kotlin("test"))
//junit5
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.0-M1")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.0-M1")
//database unit test
testImplementation("com.oracle.database.jdbc:ojdbc6:11.2.0.4")
testImplementation("mysql:mysql-connector-java:8.0.29")
testImplementation("com.github.database-rider:rider-core:1.32.3")
testImplementation("com.github.dbunit-rules:core:0.15.1")
testImplementation("com.github.dbunit-rules:junit5:0.15.1")
testImplementation("org.hibernate:hibernate-core:5.6.9.Final")
testImplementation("org.hibernate:hibernate-entitymanager:5.6.9.Final")

//Yaml parse
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3")
testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.3")

// jexl parse
testImplementation("org.apache.commons:commons-jexl3:3.2.1")
}

数据库连接和初始化配置

数据库的连接和初始化使用了JPA持久化配置,这是是Database Rider所要求的。

<?xmlversion="1.0"encoding="UTF-8"?>
<persistenceversion="2.0"xmlns="http://java.sun.com/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unitname="oracleDb1"transaction-type="RESOURCE_LOCAL">

<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

<properties>
<propertyname="hibernate.dialect"value="org.hibernate.dialect.Oracle10gDialect"/>
<propertyname="javax.persistence.jdbc.driver"value="oracle.jdbc.driver.OracleDriver"/>
<propertyname="javax.persistence.jdbc.url"value="jdbc:oracle:thin:@your_host:1521:your_dbname"/>
<propertyname="javax.persistence.jdbc.user"value="your_usernbame"/>
<propertyname="javax.persistence.jdbc.password"value="your_password"/>
<propertyname="hibernate.hbm2ddl.auto"value="create-drop"/>
<propertyname="hibernate.hbm2ddl.charset_name"value="utf-8"/>
<propertyname="hibernate.hbm2ddl.halt_on_error"value="false"/>
<propertyname="hibernate.hbm2ddl.delimiter"value=";"/>
<propertyname="hibernate.show_sql"value="true"/>

<propertyname="javax.persistence.schema-generation.database.action"value="@javax.persistence.schema_generation.database.action@"/>
<propertyname="javax.persistence.schema-generation.create-source"value="script"/>
<propertyname="javax.persistence.schema-generation.create-script-source"value="META-INF/sql/oracleDb1/ot_schema.sql"/>
<propertyname="javax.persistence.schema-generation.drop-source"value="script"/>
<propertyname="javax.persistence.schema-generation.drop-script-source"value="META-INF/sql/oracleDb1/ot_drop.sql"/>
<propertyname="javax.persistence.sql-load-script-source"value="META-INF/sql/oracleDb1/ot_data.sql"/>
</properties>

</persistence-unit>

</persistence>

@

javax.persistence.schema_generation.database.action@是一个占位符,利用了Gradle编译的替换机制,可以根据运行需要决定是否重新初始化数据库。

数据库结构和数据初始化脚本文件参考如下,来自某个Oracle教程的示例数据库,稍微作了一些改造。

需要注意的是,这里的每个SQL语句只能占用一个文本行,否则JPA运行时会报错。

--disableFKconstraints
ALTERTABLEcountriesDISABLECONSTRAINTfk_countries_regions;
ALTERTABLElocationsDISABLECONSTRAINTfk_locations_countries;
ALTERTABLEwarehousesDISABLECONSTRAINTfk_warehouses_locations;
ALTERTABLEemployeesDISABLECONSTRAINTfk_employees_manager;
ALTERTABLEproductsDISABLECONSTRAINTfk_products_categories;
ALTERTABLEcontactsDISABLECONSTRAINTfk_contacts_customers;
--------------------------------------------------------
--OT
--------------------------------------------------------
--REMINSERTINGintoREGIONS
--SETDEFINEOFF;
InsertintoREGIONS (REGION_ID,REGION_NAME) values (1,'Europe');
InsertintoREGIONS (REGION_ID,REGION_NAME) values (2,'Americas');
InsertintoREGIONS (REGION_ID,REGION_NAME) values (3,'Asia');
InsertintoREGIONS (REGION_ID,REGION_NAME) values (4,'Middle East and Africa');

--REMINSERTINGintoCOUNTRIES
--SETDEFINEOFF;
InsertintoCOUNTRIES (COUNTRY_ID,COUNTRY_NAME,REGION_ID) values ('AR','Argentina',2);
InsertintoCOUNTRIES (COUNTRY_ID,COUNTRY_NAME,REGION_ID) values ('AU','Australia',3);

测试案例、测试容器对象

为了方便组织测试,使用了测试容器和测试案例两个概念,允许有多个测试案例配置文件,每个配置文件反序列化为一个测试容器,文件里的每个配置项反序列化为一个测试。

下面分别是测试和容器的数据类定义:

dataclassConfigurableTest(
valname: String,
valdescription: String?,
valdbName: String?,
valsetup: Iterable<String>?,
valtestSql: String,
/**
* 先于assert执行,打印这些jxel表达式,用于debug
*/
valdebugs: Iterable<String>?,
valasserts: Iterable<String>?,
varteardown: Iterable<String>?
) {

}

dataclassConfigurableTestContainer(
valname: String,
valdbName: String?,
valtests: List<ConfigurableTest>
)

测试案例配置

测试案例使用Yaml文件来配置,下面是一个示例:

name: "可配置测试样例"
dbName: "oracleDb1"
tests:
-name: "简单sql和断言演示"
description: ""
dbName: ""
setup: []
testSql: "select 1 from dual"
debugs:
-'result[0]'
-'result.0'
-'result.200'
asserts: [ 'true', 'result[0] == "1"']
teardown: []
  • 可以看到,两级对象上都有dbName属性,方便内层使用不同于外层的数据库,对应到JPA持久化配置里persistence-unit的name。
  • testSql为测试要运行的核心SQL,DDL、DML均被支持。
  • asserts可以为多个JXEL表达式(值为Boolean型),用于对testSql运行后返回的result列表对象进行断言。
  • setup可以配置多个SQL,用于在运行testSql前做一些初始化工作。
  • teardown也支持多条SQL,用于在测试结束后的清理。
  • debugs同样可以配置多个JEXL表达式,运行过程中将打印其结果到控制台,用于debug。

以下是其中一个测试集(用于测试各种查询语法)的配置的片段:

name: "查询语法测试"
dbName: "oracleDb1"
tests:
-name: "测试!=消除null"
testSql: |-
SELECTt.STATE
FROMlocationst
WHEREt.STATE!='BE'
asserts: [ 'result.size() == 16' ]
-name: "测试=消除null"
testSql: |-
SELECTt.STATE
FROMlocationst
WHEREt.STATE=null
asserts: [ 'result.size() == 0' ]
-name: "测试<>运算符"
testSql: |-
SELECTt.STATE
FROMlocationst
WHEREt.STATE<>'BE'
asserts: [ 'result.size() == 16' ]
-name: "测试类型隐式转换"
testSql: |-
SELECTt.POSTAL_CODE
FROMlocationst
WHEREt.location_id='1'
asserts: [ 'result.0.equals("00989")' ]
-name: "测试||拼接字符串"
testSql: |-
SELECTt.COUNTRY_ID||'.'||t.CITY
FROMlocationst
WHEREt.LOCATION_ID=1
asserts: [ 'result.0.equals("IT.Roma")' ]

目前共使用了4个配置文件,分别用于测试DDL、字典表相关查询、函数相关查询和各种SELECT语句:

test-ddl.yml
test-dict.yml
test-function.yml
test-select.yml

测试配置的反序列化

以下是测试配置的反序列化代码,将某个资源目录下所有yml扩展名的文件最终构造为MutableMap。

由于对Kotlin不是很熟悉,代码有些啰嗦,还可以简化一下。

返回文件路径是为了利用Junit5的Test Resource特性,让IDE可以将测试对应到代码或资源,但实际没有生效,未作深入研究。

privateconstvalDEFAULT_TEST_CONFIG_PATH="/tests/"

privatefungetTestSuits(): MutableMap<Path, ConfigurableTestContainer> {
valmapper=ObjectMapper(YAMLFactory())
mapper.findAndRegisterModules()

valresourcePath=Path.of(javaClass.getResource(DEFAULT_TEST_CONFIG_PATH).toURI())

returnFiles.walk(resourcePath, 1)
.filter { path->
path.name.endsWith(".yml", true)
}
.map { path->
pathtomapper.readValue<ConfigurableTestContainer>(path.toFile())
}.toList()
.associate {
Pair(it.first, it.second)
}
.toMutableMap()
}

动态测试

接下来就进入重头戏环节了,首先定义了一个名为SqlDynamicTest的类,用于接受测试配置,再返回动态测试框架所需要的对象。

  • 入参两个对象应该可以再简化一下。
  • 可以看到Kotlin比Java方便了很多,代码写起来比较优雅,又容易规避问题。
  • 主函数是asDynamicTest,返回动态测试对象。
  • 代码应该还可以写得更好一些,部分地方还比较生硬,带着Java的痕迹。
  • 这里暂时没有把事务处理添加进来,实际上Database Rider提供了对事务处理很好的封装,可以很方便地加入事务支持。
importcom.github.database.rider.core.util.EntityManagerProvider
importorg.apache.commons.jexl3.JexlBuilder
importorg.apache.commons.jexl3.JexlEngine
importorg.apache.commons.jexl3.MapContext
importorg.junit.jupiter.api.DynamicTest
importjavax.persistence.EntityManager
importkotlin.test.assertTrue

classSqlDynamicTest(
privatevalconfig: ConfigurableTest,
privatevalsuit: ConfigurableTestContainer
) {
privatevaljexl: JexlEngine=JexlBuilder().cache(512).strict(true).silent(false).create()

privatevaldbName: Stringbylazy {
if (config.dbName?.isNotBlank() ==true) {
config.dbName
} elseif (suit.dbName?.isNotBlank() ==true) {
suit.dbName
} else {
""
}
}

privatevalemProvider: EntityManagerProviderbylazy {
EntityManagerProvider.instance(dbName)
}

privatevalem: EntityManager
get() =emProvider.em


funasDynamicTest(): DynamicTest {
if (dbName.isBlank()) {
throwRuntimeException("数据库名称不能为空,请配置到test或suit上")
}

if (config.testSql.isBlank()) {
throwRuntimeException("testSql不能为空")
}

returnDynamicTest.dynamicTest(config.name) {
exeSql(config.setup)
valresult=exeSql(config.testSql)
valcontext=MapContext()
context.set("result", result)
doDebug(config.debugs, context)
doAssert(config.asserts, context)
exeSql(config.teardown)
}
}


privatefunexeSql(sqls: Iterable<String>?) {
sqls?.forEach { sql->
valstatement=emProvider.connection().prepareStatement(sql)
statement.use { me->
me.execute()
me.close()
}
}
}

privatefunexeSql(sql: String): MutableList<Any?> {
valcleanSql=sql.trimStart()
returnif (mutableListOf("create", "drop", "grant", "insert", "update", "delete", "declare")
.any { keyword->cleanSql.startsWith(keyword, true) }
) {
exeSql(mutableListOf(sql))
mutableListOf()
} else {
valquery=em.createNativeQuery(sql)
query.resultList
}
}

privatefundoAssert(asserts: Iterable<String>?, context: MapContext) {
asserts?.forEach { assert->
valexpression=jexl.createExpression(assert)
valeval=expression.evaluate(context)
assertTrue(evalisBoolean&&eval, assert)
}
}

privatefundoDebug(debugs: Iterable<String>?, context: MapContext) {
debugs?.forEachSafely { debug->
print("$debug: ")
valexpression=jexl.createExpression(debug)
valeval=expression.evaluate(context)
printlnInGreen(eval)
}
}
}

下面这段代码,就是动态测试的入口了,怎么样,是不是很简单?

  • @TestFactory就是用来生成动态测试的注解了。
  • 整个动态测试可以是多层级的,有DynamicContainer、DynamicNode、DynamicTest等对象组合而成。
classDynamicTests {

@TestFactory
fun`可配置sql测试`(): Stream<DynamicContainer> {
valsuits=getTestSuits()
returnsuits.map { entry->
valfile=entry.key
valsuit=entry.value
DynamicContainer.dynamicContainer(
suit.name,
file.toUri(),
suit.tests.map { testConfig->
SqlDynamicTest(testConfig, suit).asDynamicTest()
}.stream()
)
}.stream()
}
}

几个辅助函数

以下是几个辅助性质的函数,部分用到了Kotlin的扩展语法,用于支持对控制台输出的文字进行着色,以及用forEach遍历时不抛出异常。

constvalANSI_RED="\u001B[31m"
constvalANSI_GREEN="\u001B[32m"
constvalANSI_RESET="\u001B[0m"

funprintlnInRed(message: Any?) =println("$ANSI_RED$message$ANSI_RESET")

funprintlnInGreen(message: Any?) =println("$ANSI_GREEN$message$ANSI_RESET")

inlinefun<T>Iterable<T>.forEachSafely(action: (T) ->Unit) {
for (elementinthis) {
try {
action(element)
} catch (e: Exception) {
printlnInRed(e.message)
}
}
}

收官

好了,到这里就分享结束了,基本上把核心的代码都一一作了介绍,怎么样,有没有对Kotlin、JUnit5、Database Rider等多了一些认识?希望本文可以在工作中帮助到你,感谢你的耐心阅读!

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/276193.html<

(0)
运维的头像运维
上一篇2025-05-10 13:19
下一篇 2025-05-10 13:20

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注