写点什么

CI+JUnit5 并发单测机制创新实践 | 京东物流技术团队

  • 2023-08-11
    北京
  • 本文字数:4932 字

    阅读完需:约 16 分钟

CI+JUnit5并发单测机制创新实践 | 京东物流技术团队

一. 现状·问题

针对现如今高并发场景的业务系统,“并发问题” 终归是必不可少的一类(占比接近 10%),每次出现问题和事故后,需要耗费大量人力成本排查分析并修复。那如果能在事前尽可能避免岂不是很香?


二. 分析原因

  • 当前并发测试多数依赖测试人员进行脚本测试,同时还依赖了研发和产品识别出并发操作的场景用例。

  • 对于并发测试,大概两条路子:


  1. 所有修改同样数据的命令式接口都测一遍?【耗费巨大测试成本】

  2. 保证黄金流程的接口,研发从头扒代码。【可能会遗漏,耗费一定研发成本】

🤔自我反思

  • 作为研发,是不是在刚开发接口时候,识别到并发场景随着单元测试阶段同时进行并发测试,这样的成本是最小的,收益是最高效的!

三. 采取措施

并发测试前置

采用 CI 持续集成机制,依靠行云流水线,底层利用 junit5 单元测试框架并发 parallel 引擎,嵌入同步数据库的自定义 unit test 脚本,将每个并发 case 维护成单元测试,数据自我闭环,可重复执行


将核心的并发场景进行及时的运行验证,最早洞察,最早验证,最小成本,最大保障!


四. 实践步骤

前提:配置 junit-platform.properties


# src/test/resources/junit-platform.propertiesjunit.jupiter.execution.parallel.enabled=truejunit.jupiter.execution.parallel.config.strategy=fixedjunit.jupiter.execution.parallel.config.fixed.parallelism=20
复制代码

单接口并发-@RepeatedTest

  • ManualCheckAppConcurrentTest 出库复核并发测试「单接口并发」-> 手动复核 10 个线程


👉 核心代码块


public class ManualCheckAppConcurrentTest extends ConcurrentTest {

@Resource ManualCheckAppService 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++; }

/** * 断言最终结果:数据无问题,线程执行无问题 */ @AfterAll public 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"}) @Override void prepareData()
复制代码

多场景并发-@Execution(CONCURRENT)

  • CheckAppConcurrentTest 出库复核并发测试「多场景并发」-> 手动复核|自动复核


👉 核心代码块


public class CheckAppConcurrentTest extends ConcurrentTest {

@Resource ManualCheckAppService manualCheckAppService; @Resource AutoCheckAppService autoCheckAppService;

/////////////////////////////////////////////////////////////////////////// // 多场景并发 /////////////////////////////////////////////////////////////////////////// @DisplayName("(多场景并发)并发测试【自动确认复核】") @Description("与手动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)") @Execution(CONCURRENT) @Test public void testAutoCheckBySo() { autoCheckAppService.autoCheckBySo(Lists.newArrayList("SO-6_6_601-1492066800186167296"), mockAutoCheckBySoDto()); }

@DisplayName("(多场景并发)并发测试【手动确认复核】") @Description("与自动复核发生并发场景,期望可能存在业务异常(自定义锁冲突发生的消息)") @Execution(CONCURRENT) @Test public void testConfirmChecked() { manualCheckAppService.confirmChecked(mockConfirmCheckedDto()); } /** * 断言最终结果:数据无问题 */ @AfterAll public 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"}) @Override void prepareData() {}
复制代码

并发单测基类-@Transactional

ConcurrentTest 建议抽出并发测试基类(主要目的:准备数据、设置路由、数据清除、独立执行)


@Tag("parallel")分组: 并发测试用例,有助于单独执行套件! ​


👉 核心代码块



@SpringBootTest(classes = WebApplication.class)@Tag("parallel")public abstract class ConcurrentTest { /** * 并发测试场景的前提数据准备 * { @Sql 数据脚本配置 } */ @Transactional @Order(0) @Rollback(false) abstract void prepareData(); /** * 设置当前线程数据源 */ @BeforeTransaction public void setThreadDataSource() { DataSourceContextHolder.clearDataSourceKey(); //多数据源,分库分表 DataSourceContextHolder.setDataSource("ds0"); }

/** * 清除数据 */ @Rollback(false) @AfterAll public 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


https://junit.org/junit5/docs/current/user-guide/#writing-tests-tagging-and-filtering


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 数

👉过程质量中并发问题趋势逐步降低



作者:京东物流 周奕儒

来源:京东云开发者社区 自猿其说 Tech

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
CI+JUnit5并发单测机制创新实践 | 京东物流技术团队_测试_京东科技开发者_InfoQ写作社区