单元测试在货拉拉的落地与实践

作者简介

  • 王满足,来自货拉拉/技术中心/质量保障部,从事质量效能平台方面的开发多年,目前主要负责部门基础设施平台与单元测试流程&精准测试的开发与建设工作。

一:背景介绍

随着货拉拉技术团队不断扩展、技术框架上从PHP迁移到Java、核心业务交易由1.0演进到2.0。由于项目的持续更新,业务逻辑愈加复杂,代码频繁变更,技术团队对工程标准和规范的诉求也就愈来愈强烈。而物流是7*24小时业务,代码问题可能导致巨大经济损失和品牌损害。我们的一些核心服务在用户下单和司机服务中起到关键作用,有些涉及各种费用计算。如果核心服务出错,整个业务可能中断,引发风险如用户流失和负面舆论。同时一些核心服务在微服务中处于核心位置,与多方关联。为确保服务稳定和服务的长生命周期可维护性,我们开始进行单元测试。

二:单测的误区与难点

为什么单测往往是是"秃子头上的虱子",却又是"房间里的大象"?

我在项目中"平添"一个单测case,与他们埋头写一千个单测、执行80%新增覆盖率标准与MR工作流都叫做写单测。我们都有光明的前途吗?

单元测试相比于近些年的各种热门的工程实践,仿佛身影有些落寞。作为一个比移动互联网兴起还要早的工程实践方法论,单元测试价值已经被无数技术专家强调。而且尽管众多技术书籍都有单元测试章节,但最佳实践仍然并不容易。原因在于单元测试虽然入门简单,但真正的工程实践却颇具挑战

2.1 误区

  • 全覆盖误区: 认为所有代码都需单元测试,实际上,并非所有代码都需测试。核心服务或模块才是单元测试的关键对象。
  • 目标误区: 单元测试仅为查找bug,虽然单元测试能协助发现bug,但其核心目的是验证代码正确性,尤其是在代码修改或重构后。
  • 一劳永逸误区: 认为单元测试只写一次,代码持续演进,单元测试亦应更新,否则可能失去其检测功能。

2.2 难点

  • 代码难度:高度耦合的代码增加测试的困难。
  • 时间压力:认为测试消耗时间,特别是在紧迫的项目周期中。
  • 成本考量:维护测试可能提高研发成本和影响交付周期。

为了充分发挥单元测试的价值,我们需明确其真正目的、选择合适的测试场景,并积极应对团队面临的各种挑战。

三:单测能力思考与建设

Java为我们提供了成熟的单元测试框架,成为代码验证的基础。通过Tekton,我们实现了自动化的CI/CD流程,其中单元测试作为第一道质量保障关。Sonar监控代码质量,特别是覆盖率,确保代码持续满足设定标准。而GitLab的MR不仅促进代码审查,还确保所有提交都经过严格的单元测试和质量评估。综合来看,这套组合为我们提供了一个强大、高效且完善的代码质量控制体系。

3.1 体系概览

3.2 建设选型

尽量选择开源方案、复用已有基础设施。单元测试技术总体架构如上图所示:

Tekton:Tekton是一个云原生的流水线工具,我们已经基于Tekton作为我们质量部门的流水线工具,比如自动化任务运行在Tekton之上;此次复用了Tekton的能力来运行单元测试运行,与覆盖率收集和上传的任务。

Gitlab Runner:Gitlab Runner是与Gitlab集成的比较好的流水线工具,我们使用Gitlab Runner来触发Tekton中任务的运行,同时判断运行结果是否通过了门禁。

SonarQube:我们使用SonarQube进行静态代码扫描,同时也利用它管理单元测试覆盖率,并采用SonarQube内置的Gateway作为子门禁。

测试框架选择: 首先第一个问题就是 junit4 和 junit5 的选择,从 junit4 到 junit5 我觉得最便利的一个好处就是可以参数化测试,并且基于参数化测试我们可以更加灵活的配置我们的参数。更好的是,junit5提供了扩展,比如我们常用的json格式。

mock 框架: Mockito,语法特别优雅,对于容器类的模拟比较合适,且对于返回值为空的函数调用也提供比较好的断言。

3.3单测流程

3.3.1 初始步骤

  • 在项目开始之初,开发者首先基于新功能或修复的需求创建一个Feature分支。

3.3.2 编写代码

  • 开发者会在这个特定分支上进行所有的开发工作。
  • 完成开发后,他们会在本地对代码进行编译和单元测试,确保新编写或修改的代码段无明显缺陷或错误。

3.3.3 代码审查

  • 完成初步的验证后,开发者将代码推送到远程仓库,并发起一个合并请求(Merge Request,简称MR)。
  • MR不仅作为代码审查的手段,还会触发单元测试流程,确保代码在与其他部分集成时能够正常运作。
  • 若测试失败,开发者需要根据反馈进行代码修复并重新提交。

3.3.4 合并代码

  • 只有当单测都成功通过时,代码才会被合并到Release分支,意味着它已经准备好进入生产环境的下一阶段。

  • 之后,代码将被进一步合并到主分支(Master),这是一个确保代码的持续集成和持续交付的关键步骤。

3.4 拉拉特色

3.4.1 拉拉三重

3.4.2 平台特色

  1. 流水线管理: 通过流水线有两个作用,一种类型的流水线是用来运行单元测试以及覆盖率汇总收集上传等操作;另外一种流水线用来触发单测运行与判断此次提交是否通过门禁标准。

  2. 配置管理: 支持配置覆盖率门禁标准,预执行脚本和设置Exclude目录等。

  3. 覆盖率管理: 将覆盖率报告汇总呈现与管理。(统计新增覆盖率,覆盖率自动更新,覆盖率指标管理)

  4. 质量门禁 : 单元测试必须全部通过作为门禁,设置新增行覆盖率为80%的标准作为门禁。

3.5 平台建设

3.5.1 新增覆盖率标准

Sonar默认门禁在代码行提交过少时会默认通过,我们对此进行了改造,严格按照覆盖率执行标准。"新代码覆盖率"指的是相比于目标分支(这里目标分支统一为master分支)的新增覆盖率,当小于设定的标准时,门禁会显示失败。

3.5.2 自定义选择

支持自定义门禁,由于各项目的单测进展不一样,选择项目加入到不同的门禁规范中,最终以80%的新增代码行覆盖为标准。

针对多module,为了统一管理,将所有的单元测试case统一放到名称为以test结尾的module文件夹中。

覆盖率的生成是采用的Jacoco方案,通过Jacoco的on-the-fly模式,在运行时动态修改类文件来收集覆盖率信息。同时指定统一路径,便于覆盖率收集。

3.5.3 MR 的应用

在当前的工作流程中,我们采用GitLab MR作为核心工具,管理和控制代码分支的合并。每当有新的MR提出,核心的评判依据是其关联的单测流水线是否顺利执行并通过。这一流程不仅确保了代码质量和功能的稳定性,而且为团队提供了一个结构化、高效的合作平台。通过这种方式,我们既维护了代码的健壮性,也保证了开发的连续性和一致性。

3.6 实现细节

3.6.1 Gitlab流水线配置
bash 复制代码
filter-job:
  only: 
    - merge_requests
  tags:
      - qaci
  stage: test
  image: harbor.xxxx.cn/basic/unit-test:latest
  script:  
    - python /workspace/trigger.py $CI_COMMIT_REF_NAME $CI_COMMIT_SHORT_SHA $GITLAB_USER_LOGIN $CI_REPOSITORY_URL $CI_PROJECT_NAME master $CI_PROJECT_ID $CI_MERGE_REQUEST_IID
    - python /workspace/filter.py $CI_COMMIT_REF_NAME

filter-job: 这是一个job的定义,job是pipeline中的一个阶段,包含一系列要执行的命令。

  • only: - merge_requests:这个job只在merge request事件发生时运行。
  • tags: - qaci:这个job只在标签为qaci的runner上运行。
  • stage: test:这个job属于测试阶段。
  • script: 这部分定义了在job中要执行的命令。其中类似$CI_COMMIT_REF_NAME为Gitlab中的环境变量。
3.6.2 覆盖率收集与对比的命令

在流水线中配置Sonarqube覆盖率收集与分支对比来获取新增覆盖率的命令可以参考如下:

ini 复制代码
mvn install -Dmaven.test.failure.ignore=true sonar:sonar \
        -Dsonar.host.url=http://xxxx.huolala.com \
        -Dsonar.login=xxxxx \
        -Dsonar.projectKey=$(params.projectKey) \
        -Dsonar.branch.name=$(params.revision) \
        -Dsonar.analysis.tektonId=$(params.tektonId) \
        -Dsonar.analysis.branch=$(params.revision) \
        -Dsonar.analysis.gitId=$(params.gitId) \
        -Dsonar.analysis.commitId=$(params.commitId) \
        -Dsonar.exclusions=$EXCLUDES \
        -Dsonar.analysis.gitlabMessage=$(params.gitlabMessage) \
        -Dsonar.branch.target=$TARGET -DskipTests
  • -Dsonar.exclusions=$EXCLUDES:设置SonarQube分析时要排除的文件或目录。
  • -Dsonar.branch.target=$TARGET:设置目标分支,一般情况下为master分支。分析的结果为Dsonar.branch.name对比Dsonar.branch.target的增量覆盖率。
  • -Dsonar.analysis:设置相关数据,可以在Sonarqube后台任务完成后,以webhook的形式把相关数据发送给第三方后台接口。
3.6.3 Sonarqube门禁改造

背景:Sonarqube门禁在设计上有项规则对于我们并不适用,"因为代码太少,有些新代码中的质量阀条件会被忽略"。有同学反馈,新增代码覆盖率为40%,而门禁阀设置的是80%,这样Sonarqube质量阀依然判定为通过。这样的一个后果就是未覆盖代码的积累,当多次MR因为代码量少而判定通过时,等release分支合并到master分支,会把之前的未覆盖代码积累到很多,导致MR不通过,这时候补充单元测试Case,在开发周期内时间已经比较晚了。

获取新增行覆盖率的示例代码:

scss 复制代码
String metricKeys="alert_status,bugs,new_bugs,vulnerabilities,code_smells,cov。
String obj = HttpRequest.get(host + "/api/measures/component")
        .basicAuth(user, password).form("login", user).form("password", password)
        .form("component", sonarProjectKey)
        .form("metricKeys", metricKeys)
        .form("additionalFields", additionalFields)
        .form("branch", branch)
        .execute().body();

根据返回的数据,获取其中的new_line_coverage字段的数据

获取门禁数据的接口的示例代码:

sql 复制代码
String obj=HttpRequest.get(host+"/api/qualitygates/project_status?projectKey="+sonarProjectKey+"&branch="+branch)
        .basicAuth(user, password).form("login", user).form("password", password)
        .execute().body();

获取其中new_line_coverage的errorThreshold字段的数据

将质量阀中获取的门禁数据与运行结果进行对比,超过设置的门禁标准的新增行覆盖率为通过。

四:建设成果与收益

4.1 整体数据概览

4.2最佳项目落地效果对比

4.3 编写成本降低

4.4 gitlab页面单测结果的可视化插件

五:总结与展望

从技术实现上,均是复用的已有的基础平台工具,没有引入新的平台。比如Sonarqube很多实践都只用来代码规范扫描,但是我们同时使用Sonarqube进行单元测试覆盖率管理和门禁卡点,同时对部分功能做了一些定制化的开发。Gitlab更多的用于代码管理,而我们更多的结合了MR与Gitlab Runner,将零散的节点能力组织串联起来成为一个流程化标准。

对于未来,我们会去探索基于AI能力的单测代码自动生成,减轻研发同学写单测的成本,同时在流程的细节上做一些优化,提升用户的整体使用体感。

相关推荐
Dnelic-3 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
世间万物皆对象9 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
Dnelic-20 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记
岳哥i1 天前
前端项目接入单元测试手册
前端·单元测试
qq_433716952 天前
Selenium+Pytest自动化测试框架 ------ 禅道实战
自动化测试·软件测试·selenium·单元测试·pytest·接口测试·压力测试
Dreams°1232 天前
【大数据测试ETL:从0-1实战详细教程】
大数据·数据仓库·python·单元测试·etl
敲代码敲到头发茂密2 天前
怎么做好白盒测试?
java·数据库·mysql·算法·单元测试·模块测试·测试覆盖率
安冬的码畜日常4 天前
【玩转 Postman 接口测试与开发2_007】第六章:Postman 测试脚本的创建(下):预请求脚本及环境变量在多个请求自动运行中的应用
测试工具·postman·测试·runner·api测试·自动测试
Dreams°1235 天前
【大数据测试HDFS + Flask详细教程与实例】
大数据·功能测试·hdfs·单元测试·flask
CSXB996 天前
三十八、Python(pytest框架-上)
python·功能测试·测试工具·单元测试·pytest