最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

有赞单元测试实践

  • 2020-03-15
  • 本文字数:4808 字

    阅读完需:约 16 分钟

有赞单元测试实践

一、概述


单元测试是指对软件中的最小可测试单元进行检查和验证。单元在质量保证中是非常重要的环节,根据测试金字塔原理,越往上层的测试,所需的测试投入比例越大,效果也越差,而单元测试的成本要小的多,也更容易发现问题。

1.1 有赞单元测试 1.0 架构


以有赞中台某应用为例,应用部署是微服务架构,对外提供 dubbo 服务,当前的单元测试,采用了分层测试框架,根据代码的分层,分为 Service 层测试,Biz 层测试,外部服务访问层测试,DAO 测试,Redis 访问层测试,每一层均使用 mock 框架屏蔽下层的具体实现。

1.2 单元测试的过程


单元测试的编写,主要包含以下几个阶段:


  1. 数据准备:在编写测试用例前,需要依赖到一些数据,数据来源一般是数据库,而构造数据,又不能依赖 DAO 层的代码,需要使用原生 jdbc 去插入数据,测试代码编写效率低。

  2. 构造参数及打桩(stub):调用方法需要传递入参,有时候一个入参十几个参数需要 set,set 方法写完,代码已经写了十来行了。

  3. 执行测试:这一步比较简单,直接调用被测方法即可。

  4. 结果验证:这里除了验证被测方法的返回值外,还需要验证插入到数据库中的数据是否正确,某外部方法被调用过 n 次或未调用过。

  5. 必要的清理:对打桩进行清理,对数据库脏数据进行清理。

二、 痛点

2.1 重构代码需要改写大量单元测试用例

对外的 Service 接口在不变的情况下,对内部实现进行重构,这时候头痛的问题来了,大量的 Service 层单元测试,biz 层单元测试都要重写;有时候 Service 调用 biz 层接口时,参数传错了,而由于开发人员编写单元测试时不规范,参数匹配使用了 anyxxx(),导致参数传错的 bug 未被发现。

2.2 测试库数据随意修改导致的单元测试不稳定

DAO 层单元测试直连测试库,由于测试库的数据可以被任意修改,从而导致测试依赖的数据被更改,单元测试不通过,另外开发在编写单元测试时,没有清理意识,导致测试库大量垃圾数据。

2.3 单元测试结果校验缺失

例如一个 SaveItem() 接口,执行完成后除了要验证执行成功以外,还应该验证落库数据的正确性,而编写这部分测试代码需要大量的使用原生 jdbc 接口查询 sql,并逐字段验证正确性,代码编写效率低下。

三、几个常用的测试框架的简介

3.1 数据层单元测试框架 DbUnit

可以优雅的构造 DB 层的初始化数据,例如:


<?xml version='1.0' encoding='UTF-8'?><dataset>  <employee employee_uid='1'      start_date='2001-11-01'           first_name='Andrew'      ssn='xxx-xx-xxxx'      last_name='Glover' /></dataset>
复制代码


其中 employee 是要构造数据的表名,后面的键值对是列名及对应的值,需要注意的是,第一行必须包含完整的字段名,否则加载的数据中全部会缺失某些字段。

3.2 嵌入式的内存数据库 H2

非常适合在测试程序中使用,程序关闭时自动清理数据,H2 数据库的表结构初始化是通过 jdbc:initialize-database 标签实现的,单元测试中使用 H2 数据库非常简单,仅需修改 jdbc 连接即可。


引入依赖:


<dependency>   <groupId>com.h2database</groupId>   <artifactId>h2</artifactId>   <version>1.4.191</version>   <scope>test</scope></dependency>
复制代码


数据源连接:


spring.datasource.url=jdbc:h2:mem:testspring.datasource.driver-class-name=org.hsqldb.jdbcDriverspring.datasource.username=rootspring.datasource.password=
复制代码


schema 初始化:


<jdbc:initialize-database data-source="dataSource" ignore-failures="NONE">    <jdbc:script location="classpath:h2/schema.sql" encoding="UTF-8"/>  </jdbc:initialize-database>
复制代码

3.3 Spring 小扩展 springockito

它简化了在集成测试的相关上下文 XML 文件中创建 mockito mocks 的方法。


<beans xmlns="http://www.springframework.org/schema/beans"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xmlns:mockito="http://www.mockito.org/spring/mockito"  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  http://www.mockito.org/spring/mockito http://www.mockito.org/spring/mockito.xsd">...  <mockito:mock id="accountService" class="org.kubek2k.account.DefaultAccountService" />..</beans>
复制代码

3.4 spring 官方测试框架 spring-test

目前主流的开发框架都在使用 spring 框架管理 bean,在测试代码中,我们通用期望能够使用 spring 框架,spring-test 框架帮助我们解决 bean 的注入问题。


@ContextConfiguration(locations = "/test-context.xml",             loader = SpringockitoContextLoader.class) public class CustomLoaderXmlApplicationContextTests {  // class body...}
复制代码


"/test-context.xml" 指定了测试类运行需要加载的 spring 配置文件路径, SpringockitoContextLoader指定了加载配置的类,这两个一起用可以支持在使用 spring xml 配置的同时可以将 mockito 生成的 mock 对象 bean 注入 spring 上下文中。

3.5 支持静态方法 mock 的 mock 框架 powermock

支持静态方法 mock,同时兼容 mockito,powermock 示例:


@RunWith(PowerMockRunner.class)@PrepareForTest( { YourClassWithEgStaticMethod.class })public class YourTestCase {...}
复制代码

四、有赞基于 springtest+ut+powermock 的测试框架


有赞单元测试框架,数据库层使用 h2 数据库代替测试库,隔离单元测试数据与测试库数据,在单元测试结束后自动清理数据,避免污染测试库数据及被测试库数据影响,基于 DbUnit 可以通过 xml 构造 DB 层初始化数据,实现测试代码与测试数据分离,依赖 spring jdbc的初始化脚本初始化 h2 数据库的表结构。

4.1 数据准备

单测依赖的 Db 数据,通过添加测试方法监听器,在 Junit 执行前通过 DbUnit 工具类,加载初始化文件,写入 H2 数据库;单测的入参,通过 param.json 文件,以 json 格式编写入参数据,利用工具类读取文件并 json 反序列化为目标 Class 实例。


H2 数据库的表结构,则是通过上文提到的 jdbc:initialize-database 初始化的,开发同学必须保证此 schema 与线上结构的一致性,否则会导致单测失败。


添加方法监听器


@TestExecutionListeners({JunitMethodListener.class})


这是自定义的监听器,在执行前后执行自定义逻辑,包括数据准备、验证和清理。


public class JunitMethodListener extends AbstractTestExecutionListener {
@Override public void beforeTestMethod(TestContext testContext) throws Exception {
Method jdkMethod = testContext.getTestMethod(); if (jdkMethod == null) { return; }
Object classInstance = testContext.getTestInstance(); if (!(classInstance instanceof JunitRunner)) { return; }
TestMethod testMethod = jdkMethod.getAnnotation(TestMethod.class); if (testMethod == null) { return; }
JunitRunner runner = (JunitRunner) classInstance; runner.init(); if (testMethod.enablePrepare()) { TestRunnerTool.prepare(testMethod, runner); } }
@Override public void afterTestMethod(TestContext testContext) throws Exception { boolean hasException = (testContext.getTestException() != null) ? true : false;
Method jdkMethod = testContext.getTestMethod(); if (jdkMethod == null) { return; }
Object classInstance = testContext.getTestInstance(); if (!(classInstance instanceof JunitRunner)) { return; }
TestMethod testMethod = jdkMethod.getAnnotation(TestMethod.class); if (testMethod == null) { return; }
JunitRunner runner = (JunitRunner) classInstance; if (!hasException && testMethod.enableCheck()) { TestRunnerTool.check(testMethod, runner); }
if (testMethod.enablePrepare()) { //清理数据 TestRunnerTool.clean(testMethod, runner); } }}
复制代码


以下是单元测试代码示例,enablePrepare 声明需要准备数据,prepareDateConfig 声明数据准备的文件路径,prepareDateType 是数据准备的类型,xml -> DB,当然也支持更多的文件类型,如 csv,xls。


  @TestMethod(      enablePrepare = true,      prepareDateType = PrepareDataType.XML2DB,      prepareDateConfig = {PREPARE_XML_FILE_USER}  )  @Test  public void test_updateUser(){    ... 具体代码省略  }
复制代码

4.2 桩代码相关框架

为了使被测代码能够独立运行、并控制被测代码的执行路径,我们需要对外部依赖(包括中间件、静态函数、外部服务)进行 mock,mock 框架依赖的是 PowerMockmockito,利用 spring-test 集成 springockito 将 mock 的 bean 注入到 Spring 上下文中。


使用 PowerMock 运行 Junit 单元测试


@RunWith(PowerMockRunner.class)@PowerMockIgnore({ "javax.management.*", "javax.net.ssl.*"})
复制代码


PowerMock 集成 Spring TestContext 框架


@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)@ContextConfiguration(    loader = SpringockitoContextLoader.class,    locations = {        "classpath:applicationContext-test.xml" })
复制代码

4.3 结果验证

结果验证,包括两部分,一个是被测函数的返回值,这个需要编写者自行验证,另一个是写入数据库的值,这部分是通过在方法上添加注解,告诉单元测试框架要验证的语句,执行验证语句并与期望值比较。


单元测试方法示例:


  @TestMethod(      enablePrepare = true,      prepareDateType = PrepareDataType.XML2DB,      prepareDateConfig = { PREPARE_XML_FILE_USER},      enableCheck = true,      checkConfigFiles = {"/saveUserCheck.json"}  )  @Test  public void test_updateUser() throws IOException {    UserParam param = MockUtil.fromFile(        "/param.json",        UserParam.class);
... }
复制代码


saveUserCheck.json 文件内容示例


   {  "check.type": "DB_CHECK",  "check.desc": "检查 更新结果正确性",  "check.sql.query": "select status from user where user_id=1",  "check.expected.data": [   {    "status": 1   }  ] }
复制代码

4.4 以下是单元测试基类的示例代码

五、总结

第二部分提到的几个痛点,通过我们的 zantest 测试组件,我们完美的解决这几个问题,通过注解方式,实现了配置数据与测试代码的分离,简化测试代码编写,隔离测试环境数据库,并编写了一套测试示例进行推广。

5.1 关于内部重构的痛点,我们基于有赞单元测试框架解决了这个问题


在单元测试 1.0 版本时,我们分别对 Service,innerBeanA,innerBeanB,UserDAO 写单元测试,当 Service 层输入输出不变,内部重构时,这几个类的单元测试都要重构,而在单元测试 2.0 版本时,由于被测函数只有 Service,通过桩代码控制 Service 对 innerBeanA,innerBeanB,UserDAO 的调用,从而覆盖 inner 层和 DAO 层,重构时只需要改写 Service 层代码即可。

5.2 测试库数据被随意修改

数据准备不再依赖测试库,而是通过文件构造测试数据,例如上文的 xml 格式,为方便测试数据的构造,同时也支持更多的数据格式,例如 csv,可以方便的将线上数据导出作为测试用例。

5.3 单元测试结果校验

一方面开发仍然需要自行校验函数的返回值,校验 mock 函数是否被执行,另一方面对数据库数据更改的验证可以直接通过注解声明校验的 sql 文件路径即可。


相关链接


DbUnit:http://dbunit.sourceforge.net/howto.html


H2:http://www.h2database.com/html/quickstart.html


springockito:https://github.com/springockito/springockito


spring-test:https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testing


powermock:https://github.com/powermock/powermock


2020-03-15 20:192396

评论 1 条评论

发布
用户头像
有源代码吗?
2023-02-22 10:44 · 广东
回复
没有更多了
发现更多内容

Python的流程控制,你真的会了吗?(一)

霍格沃兹测试开发学社

我们是如何测试人工智能的(一)基础效果篇(内含大模型的测试内容)

测吧(北京)科技有限公司

劳动力规划:对企业加速运营的未来展望

智达方通

企业管理 企业转型 全面预算管理 劳动力规划

昇思之路,从AI基础软件到生态繁花

脑极体

AI

跨界创新,数字赋能:探索低代码平台的多元化应用场景

优秀

低代码 低代码开发平台 低代码平台 低代码应用场景

聚道云软件连接器:助力企业财务效率提升的成功案例

聚道云软件连接器

案例分享

10分钟带你了解 Linux 系统中的 Top 命令

霍格沃兹测试开发学社

ETL中如何自定义规则

RestCloud

数据同步 ETL 数据规则

被 AI 写的游戏代码砸中是什么感觉 | 10 分钟打造你的超级 AI 编码助手

阿里云云效

阿里云 云原生 通义灵码

深信服:借助观测云实现全链路可观测性

观测云

链路

SQLite的第一版不过是在GDBM上套了个壳

胡译胡说

sqlite 数据库 历史 KV存储

精挑细选:哪款PLM软件最适合您的企业?全面对比10大热门产品

PingCode

项目管理 产品经理 PLM软件

无需注册即可使用 ChatGPT;Poe 创始人:大模型幻觉是创业公司的机会丨RTE 开发者日报 Vol.176

声网

Octavia Venture 成立,打造数十亿美元规模的 AI 价值体系

股市老人

谈谈我对 AIGC 趋势下软件工程重塑的理解

阿里云云效

阿里云 云原生 AIGC 通义灵码

体育变革:一位年轻创业者燃体育直播的新火花

软件开发-梦幻运营部

聚道云助IT公司破解数据同步难,高效转型新利器!

聚道云软件连接器

案例分享

“不知今夕是何年”的周基年解法|得物技术

得物技术

Java 程序员 前端 后端 企业号 4 月 PK 榜

Flutter应用发布流程详解:从开发到上架一站式指南

雪奈椰子

解密通义灵码:软件研发工具的“大脑”

阿里云云效

阿里云 云原生 通义灵码

微调工程师岗位可能并不存在,但使用 AI 编码工具已经成为刚需

阿里云云效

阿里云 云原生 AIGC 通义灵码

Octavia Venture 成立,打造数十亿美元规模的 AI 价值体系

股市老人

Golang数据库事务实践

俞凡

golang

知识图谱在五大智能领域的应用

悦数图数据库

知识图谱

从 Redis 开源协议变更到 ES 国产化:一次技术自主的机遇 记某客户的一次无缝数据迁移

极限实验室

console Gateway easysearch

inBuilder低代码平台新特性推荐-第十七期

inBuilder低代码平台

开源 低代码

Java 的诞生——从 Oak 到 Java

胡译胡说

Java 历史

以太坊测试币怎么领?Holesky&Sepolia水龙头盘点

加密先生

Penpad Season 2 质押突破350ETH,还有望获Scroll生态空投

股市老人

Flutter应用在苹果商店上架前的准备工作与注意事项

Flink Checkpoint 状态后端详解:类型、特性对比及场景化选型指南

木南曌

flink 实时计算

有赞单元测试实践_文化 & 方法_国庆_InfoQ精选文章