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

简介: 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,则是渐进式重构有效开展的必选方法。

相关文章
|
1月前
|
监控 测试技术
如何进行系统压力测试?
【10月更文挑战第11天】如何进行系统压力测试?
100 34
|
1月前
|
存储 监控 网络协议
服务器压力测试是一种评估系统在极端条件下的表现和稳定性的技术
【10月更文挑战第11天】服务器压力测试是一种评估系统在极端条件下的表现和稳定性的技术
109 32
|
11天前
|
缓存 监控 测试技术
全网最全压测指南!教你如何测试和优化系统极限性能
大家好,我是小米。本文将介绍如何在实际项目中进行性能压测和优化,包括单台服务器和集群压测、使用JMeter、监控CPU和内存使用率、优化Tomcat和数据库配置等方面的内容,帮助你在高并发场景下提升系统性能。希望这些实战经验能助你一臂之力!
25 3
|
15天前
|
前端开发 JavaScript 测试技术
前端小白逆袭之路:如何快速掌握前端测试技术,确保代码质量无忧!
【10月更文挑战第30天】前端开发技术迭代迅速,新手如何快速掌握前端测试以确保代码质量?本文将介绍前端测试的基础知识,包括单元测试、集成测试和端到端测试,以及常用的测试工具如Jest、Mocha、Cypress等。通过实践和学习,你也能成为前端测试高手。
33 4
|
20天前
|
编解码 安全 Linux
网络空间安全之一个WH的超前沿全栈技术深入学习之路(10-2):保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali——Liinux-Debian:就怕你学成黑客啦!)作者——LJS
保姆级别教会你如何搭建白帽黑客渗透测试系统环境Kali以及常见的报错及对应解决方案、常用Kali功能简便化以及详解如何具体实现
|
24天前
|
敏捷开发 监控 jenkins
自动化测试之美:打造高效的软件质量保障体系
【10月更文挑战第20天】在软件开发的海洋中,自动化测试如同一艘精准的导航船,引领项目避开错误的礁石,驶向质量的彼岸。本文将扬帆起航,探索如何构建和实施一个高效的自动化测试体系,确保软件产品的稳定性和可靠性。我们将从测试策略的制定、工具的选择、脚本的编写,到持续集成的实施,一步步描绘出自动化测试的蓝图,让读者能够掌握这一技术的关键要素,并在自己的项目中加以应用。
28 5
|
1月前
|
设计模式 关系型数据库 测试技术
进阶技巧:提高单元测试覆盖率与代码质量
【10月更文挑战第14天】随着软件复杂性的不断增加,确保代码质量的重要性日益凸显。单元测试作为软件开发过程中的一个重要环节,对于提高代码质量、减少bug以及加快开发速度都有着不可替代的作用。本文将探讨如何优化单元测试以达到更高的测试覆盖率,并确保代码质量。我们将从编写有效的测试用例策略入手,讨论如何避免常见的测试陷阱,使用mocking工具模拟依赖项,以及如何重构难以测试的代码。
56 4
|
2月前
|
Linux
kickstart自动安装系统 --DHCP 配置及测试
PXE+Kickstart自动安装系统需配置DHCP服务器分配IP。dhcpd.conf示例:设置更新样式、忽略客户端更新、指定下一服务器及启动文件。定义子网、网关、掩码、动态地址池并预留特定MAC地址。重启xinetd、NFS、DHCP服务,确保新服务器与Kickstart服务器在同一网络,避免误装其他机器。注意隔离测试网络以防干扰生产环境。
80 18
|
2月前
|
测试技术 持续交付 Python
自动化测试之美:打造高效的软件质量保障体系
【9月更文挑战第25天】在软件开发的海洋中,自动化测试是一艘能够引领我们高效航行的帆船。它不仅能帮助我们发现缺陷,更是一个持续集成和持续部署(CI/CD)过程中不可或缺的部分。本文将通过浅显易懂的语言和实际代码示例,引导读者理解自动化测试的价值,并学会如何实施它,从而提升软件的质量与开发效率。
41 4
|
1月前
|
存储 Linux 网络安全
Kali 渗透测试:Meterpreter在Windows系统下的使用
Kali 渗透测试:Meterpreter在Windows系统下的使用