10 月 23 - 25 日,QCon 上海站即将召开,现在购票,享9折优惠 了解详情
写点什么

有赞单元测试实践

  • 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:192840

评论 1 条评论

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

MIAOYUN获评“2023年度一云多芯稳定安全运行优秀案例”

MIAOYUN

解决方案 信创 中国信通院 信创云 可信云大会

高性能网络建设指南,《智算中心网络架构白皮书》开放下载

Baidu AICLOUD

大模型训练 高性能网络 RDMA

etl engine 监控面板 为管理者掌握平台运行情况,决策执行方案提供即时数据支撑

weigeonlyyou

数据交换 物联网 数据采集 ETL Kafka ETL

NFTScan 正式上线 zkSync NFTScan 浏览器和 NFT API 数据服务

NFT Research

NFT\

蓝牙智能设备数据采集平台化方案 | 京东云技术团队

京东科技开发者

数据采集 企业号 8 月 PK 榜 蓝牙智能设备

数据智能:加速企业数字化转型

软通咨询

数据智能 #人工智能 数字化咨询

LED透明屏清晰度受什么影响

Dylan

广告 案例 信息 LED显示屏 屏幕

低代码是什么意思?

优秀

低代码

LangChain:打造自己的LLM应用 | 京东云技术团队

京东科技开发者

langchain LLM模型 企业号 8 月 PK 榜

数字化转型背景下经管大数据课程教学能力进阶提升训练营,线下培训圆满收官!

ModelWhale

人才培养 学科交叉 师资培训 教育数字化

EPM时代,国产化替代夺回话语权

智达方通

企业管理软件 EPM 智达方通 全面预算管理 智达方通EPM

GaussDB技术解读系列之SQL Audit,面向应用开发的SQL审核工具

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 8 月 PK 榜

FastAPI简介:快速理解Python Web框架的新标杆

Liam

Python 程序员 后端 web开发 FastApi

医疗知识图谱问答——文本分类解析

北桥苏

Python 聊天机器人 neo4j 图数据库 知识图谱

一种轻量级定时任务实现 | 京东云技术团队

京东科技开发者

定时任务 系统稳定性 轻量级 企业号 8 月 PK 榜

如何为物联网设备注入“华为云+鸿蒙DNA”?

华为云开发者联盟

云计算 后端 华为云 华为云开发者联盟 企业号 8 月 PK 榜

华为云第二期线下meetup·北理工站圆满落幕

华为云开源

开源

代码随想录Day36 - 贪心算法(五)

jjn0703

第二期开源答题挑战,看看你是什么级别吧!

开放原子开源基金会

开源 挑战 答题

华为云与医药企业共话AI 助力医药行业数字化转型和创新发展

新消费日报

权威认证 I ONES 连续5年通过可信云企业级 SaaS 服务评估

万事ONES

分布式服务高可用实现:复制 | 京东物流技术团队

京东科技开发者

数据库 复制 高可用设计 分布式服务 企业号 8 月 PK 榜

什么是数字化?数字化转型概念是怎么兴起的?

优秀

数字化转型 数字化

如何通过Python线程池实现异步编程?

互联网工科生

Python 线程池

站在营销的角度浅谈直播行业

山东布谷网络科技

直播 直播app 直播APP源码

盘点一对一直播源码iOS系统维持平台稳定功能(一):弹性扩缩容

山东布谷科技

软件开发 源码搭建 iOS SDK 一对一直播源码 弹性扩缩容

方法论揭秘|研发数字化转型,这家保险企业做对了什么?

万事ONES

华为开发者大会2023即将召开:HarmonyOS 4 小艺或将迎来全新升级

最新动态

中国出海企业如何防范恶意退货欺诈

极客天地

落实《中国人民银行业务领域数据安全管理办法》,极盾科技是怎么做的?

极盾科技

数据安全

全新升级!腾讯云大数据ES Serverless服务开启日志分析新体验

腾讯云大数据

elastic

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