复杂系统如何保障代码质量?让测试先行

简介: TDD(Test Driven Development)是一种强调测试先行的开发方式,通过编写单元测试用例,有效保障存量复杂系统在开发、重构上的质量。本文通过分析现有测试方法面临的问题,分享如何使用GTest框架进行单元测试,以及在单元测试中的一些实践心得。

image.png

作者 | 棋李
来源 | 阿里技术公众号

一 业务背景

高德在线导航服务作为有很强业务特性和多年历史积累的存量系统,不可避免的存在大量的不合理代码,而业务演进对系统性能、算法、底层架构等不断提出更高要求,存量的各种业务代码和算法、架构快速演进的诉求存在严重冲突,如何有效保障质量地进行快速重构式演进,成为业务发展面临的首要工程难题。

二 现有质量保障方法问题与分析

1 现有测试方法的问题

常规方法是对新老服务批量进行请求比较diff,这种方式简单有效,是我们一直在用的方法,但存在以下问题:

  • 无效diff问题:以公交规划引擎为例,依赖步导引擎、搜索、公交突发事件、路况等多个下游服务,获取结果的差异导致很多无效diff。
  • 运行时间较长:case量较多时运行时间较长,在10分钟级别。由于这一步成本较高,一般开发人员跑diff的频率不会太高,无法进行"每次一小步"的测试。
  • 排查困难:当发现diff后进行排查非常困难,因为是整个请求级别的diff,中间步骤可能都存在问题。

2 业界主流方法实践

ThoughtWorks、Google等公司使用TDD方式进行敏捷开发,通过编写单元测试用例保障开发、重构的质量,目前已经成为主流最佳实践。

三 单元测试介绍

1 什么是单元测试?

单元测试是对一个模块、一个函数或者一个类进行正确性检验的测试工作。

测试的粒度更小更轻量,运行时间在秒级,特别适合渐进式重构中的"每次一小步"的质量保障。

由于单元测试用例针对的是一个函数、类更细粒度的目标,所以当某个用例不通过时,可以快速锁定问题点。

2 单元测试框架

常见单元测试框架有 xUnit 系列,多种语言都有对应实现,如CppUnit、JUnit、NUnit...

GTest是Google开发的单元测试框架,此框架具有一些高级功能,如death test, mock等。

我们选择的是GTest框架。

3 单元测试、重构、TDD与敏捷

TDD(Test Driven Development)是强调测试先行的开发方式,这种方式的好处在于编写任何函数、修改任何代码时可以通过编写一个单元测试用例代码来表达要实现的代码功能,一个测试用例本身就是一个代码表达的需求。而积累起来的测试用例可以有效保障开发及后续重构演进的质量。

重构和TDD是敏捷方法的核心构成要素,脱离了TDD的敏捷是危险的,没有用例保障的重构一旦启动,就像一匹脱缰的野马。而单元测试和TDD则是缚住野马的缰绳。

四 公交服务单元测试实践

1 GTest框架集成

Git库地址:https://github.com/google/googletest

GTest框架集成非常简单,把googletest库加入到工程中, 增加链接 libgtest 即可:

image.png

通过如下代码即可驱动用例执行:

int RCUnitTest::Excute()
{
  int argc = 2;
  char* argv[] = {const_cast<char*>(""), const_cast<char*>("--gtest_output=\"xml:./testAll.xml\"")};
  ::testing::InitGoogleTest(&argc, argv);

  return RUN_ALL_TESTS();

开关控制:为避免影响到正式版本, 可以考虑通过编译控制,也可以增加一个配置项开关。

我们在使用时是在入口处通过一个配置项控制是否触发单元测试用例,编译时默认只链接入口文件,需要运行单元测试时添加上单元测试用例文件进行链接运行。

2 测试代码编写

通过实现一个Test类的派生类,然后使用TEST_F宏添加测试函数即可,如下示例:

class DateTimeUtilTest : public ::testing::Test
{
protected:
    virtual void SetUp()
{
    }

virtual void TearDown()
{
    }
};

TEST_F(DateTimeUtilTest, TestAddSeconds_leap)
{
    //闰年测试 2020-02-28
    tm tt;
    tt.tm_year = (2020 - 1900);
    tt.tm_mon = 1;
    tt.tm_mday = 28;
    tt.tm_hour = 23;
    tt.tm_min = 59;
    tt.tm_sec = 50;

    DateTimeUtil::AddSeconds(tt, 30);
    EXPECT_TRUE(tt.tm_sec == 20);
    EXPECT_TRUE(tt.tm_min == 0);
    EXPECT_TRUE(tt.tm_hour == 0);
    EXPECT_TRUE(tt.tm_mday == 29);
    EXPECT_TRUE(tt.tm_mon == 1);

    //非闰年测试 2019-02-28
    tm tt1;
    tt1.tm_year = (2019 - 1900);
    tt1.tm_mon = 1;
    tt1.tm_mday = 28;
    tt1.tm_hour = 23;
    tt1.tm_min = 59;
    tt1.tm_sec = 50;
    DateTimeUtil::AddSeconds(tt1, 30);
    EXPECT_TRUE(tt1.tm_sec == 20);
    EXPECT_TRUE(tt1.tm_min == 0);
    EXPECT_TRUE(tt1.tm_hour == 0);
    EXPECT_TRUE(tt1.tm_mday == 1);
    EXPECT_TRUE(tt1.tm_mon == 2);
};

测试用例执行效果:

image.png

目前公交引擎已经积累了23个模块测试用例,基本覆盖了寻站、寻路、ETA、票价、风险停运等核心功能,持续积累中。通过单元测试保障,每个版本开发活动中都在进行渐进式重构活动,能够有效保障质量,提测迭代次数和线上新增代码引入问题数量持续较低。

image.png

3 问题与难点

数据依赖问题

在线导航引擎是对数据重度依赖的业务,多组数据结构之间互相关联,字段繁多,很难脱离数据构建有效的单元测试。通过mock方式构造假数据成本很高。而数据变化将导致用例不能通过。

我的实践:

能够简单构造假数据的通过构造假数据来搞定。

对于很难构建假数据的情况,直接使用真实数据即可。数据变化可能导致这部分用例不通过,没有关系,只需要保障在每次重构前把相关的用例调通即可,这样仍可以确保重构过程的质量。即:不需要做到用例随时随地都能运行通过,而是保证重构前后都可以通过。

4 常见错误认知

对于没有真正实践过单元测试和TDD开发方式的同学来说,有一些认知上的常见误区,比如:

开发时间都不够, 哪有时间编写单元测试?

我的理解:

  • 首先TDD的开发方式强调的是测试先行,编写测试代码是在前面的,这个过程等于是理解需求的过程。即想清楚你要实现的是什么功能?这个测试代码是理清需求的产物, 如此而已,不存在更多时间成本。
  • TDD开发方式属于典型的一次投入,持续受益的事情,用例积累越多,越容易在早期发现问题,重构有了质量保障,代码越来越整洁清晰,开发同学们再也不用哀叹历史代码。

历史代码那么多,怎么补单元测试?

那就从添加第一个用例开始。我的做法是对应本次修改涉及到的代码添加用例,逐步积累。

添加用例的过程是理解现有代码的过程,对于存量的历史代码,各种硬性编码侵入,各种耦合,全局变量或长生命周期大对象,通过编写单元测试用例能够有效理清函数真正的输入输出,也为重构增加了有效保障。

五 存量复杂系统代码渐进式重构

对于我们一线码农,每天大部分时间都在和代码打交道,如果你维护的代码结构合理、易读易扩展,那么恭喜你!但大部分情况我们面对的是存在各种历史"积淀"的存量工程,各种牵一发而动全身,这种情况下小改动还可以靠多花时间,认真仔细来搞定,但想要做一些大的系统升级就难了。

而对于巨型业务系统来说,重写在成本和质量控制方面显得更不现实。那么设置几个大的节点,通过渐进式重构逐渐优化,变量变为质变,是综合来看最优的方式。

而单元测试和TDD,则是渐进式重构有效开展的必选方法。

相关文章
|
3月前
|
机器学习/深度学习 人工智能 测试技术
EdgeMark:嵌入式人工智能工具的自动化与基准测试系统——论文阅读
EdgeMark是一个面向嵌入式AI的自动化部署与基准测试系统,支持TensorFlow Lite Micro、Edge Impulse等主流工具,通过模块化架构实现模型生成、优化、转换与部署全流程自动化,并提供跨平台性能对比,助力开发者在资源受限设备上高效选择与部署AI模型。
401 9
EdgeMark:嵌入式人工智能工具的自动化与基准测试系统——论文阅读
|
7月前
|
数据采集 算法 数据管理
频标频稳比对测试系统重新定义测量边界
在上海张江实验室的超净间里,一束激光正以每秒 30 万公里的速度穿越真空腔,与原子跃迁频率进行着纳米级的较量。而在千里之外的西安高新区,一台黑色金属机箱内,SYN5609A 型频标比对测量系统正以同样的精度,为这场量子级的时间竞赛提供着基准坐标。这台看似普通的仪器,正在用双混频时差技术,将人类对时间的掌控精度推向新的维度。
|
6月前
|
人工智能 缓存 自然语言处理
别再手搓测试数据了!AE测试数据智造系统揭秘
本文介绍如何通过构建基于大语言模型的测试数据智造Agent,解决AliExpress跨境电商测试中数据构造复杂、低效的问题,推动测试效率提升与智能化转型。
别再手搓测试数据了!AE测试数据智造系统揭秘
|
5月前
|
人工智能 物联网 测试技术
智能化测试基础架构:软件质量保障的新纪元
本文介绍了智能化测试基础架构的核心构成与优势。该架构融合AI、领域工程与自动化技术,包含智能测试平台、测试智能体、赋能引擎和自动化工具链四部分,能自动生成用例、调度执行、分析结果,显著提升测试效率与覆盖率。其核心优势在于实现专家经验规模化、质量前移和快速适应业务变化,助力企业构建新一代质量保障体系。建议从构建知识图谱和试点关键领域智能体起步,逐步推进测试智能化转型。
|
8月前
|
jenkins 测试技术 Shell
利用Apipost轻松实现用户充值系统的API自动化测试
API在现代软件开发中扮演着连接不同系统与模块的关键角色,其测试的重要性日益凸显。传统API测试面临效率低、覆盖率不足及难以融入自动化工作流等问题。Apipost提供了一站式API自动化测试解决方案,支持零代码拖拽编排、全场景覆盖,并可无缝集成CI/CD流程。通过可视化界面,研发与测试人员可基于同一数据源协作,大幅提升效率。同时,Apipost支持动态数据提取、性能压测等功能,满足复杂测试需求。文档还以用户充值系统为例,详细介绍了从创建测试用例到生成报告的全流程,帮助用户快速上手并提升测试质量。
|
10月前
|
JSON 前端开发 API
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
553 5
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
|
11月前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的大学生体质测试管理系统设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
214 1
|
敏捷开发 安全 测试技术
掌握单元测试:确保代码质量的关键步骤
单元测试是确保代码质量、提升可维护性和可靠性的重要手段。本文介绍了单元测试的基本概念、重要性及最佳实践,包括测试驱动开发(TDD)、保持测试独立性、使用断言库和模拟依赖等,旨在帮助开发者掌握单元测试技巧,提高开发效率。