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

简介: 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月前
|
设计模式 安全 测试技术
【软件设计师备考 专题 】系统实施:程序设计和系统测试
【软件设计师备考 专题 】系统实施:程序设计和系统测试
64 0
|
1月前
|
存储 安全 测试技术
软件测试:确保代码质量与用户满意度的关键步骤
软件测试:确保代码质量与用户满意度的关键步骤
|
6天前
|
消息中间件 网络协议 物联网
如何入门做物联网系统压测?
【4月更文挑战第18天】物联网系统在架构、网络模式、通信协议等方面与传统的互联网系统有所区别。因此,传统的性能测试方法不能直接套用到物联网系统中。
77 13
如何入门做物联网系统压测?
|
7天前
|
敏捷开发 Devops 测试技术
深入探索软件测试:保障质量的终极策略
【4月更文挑战第18天】在软件开发生命周期中,确保最终产品的质量至关重要,而软件测试则是达成这一目标的关键步骤。本文将探讨软件测试的多维度作用,包括其在不同开发阶段的应用、面临的挑战以及未来趋势。通过分析自动化测试工具的选择、测试用例设计的最佳实践和持续集成的重要性,文章为读者提供了一套全面的质量保证策略,旨在帮助团队提升测试效率并优化产品质量。
|
4月前
|
Ubuntu 测试技术 Linux
软件测试/测试开发|Ubuntu系统常用文件管理命令详解
软件测试/测试开发|Ubuntu系统常用文件管理命令详解
27 1
|
24天前
|
SQL 敏捷开发 算法
深入白盒测试:静态分析与代码质量保障
【4月更文挑战第2天】 随着软件开发的复杂性日益增加,确保代码质量和功能正确性成为开发流程中不可或缺的一环。白盒测试作为一种重要的软件测试方法,允许测试者通过检查程序内部结构、设计和编码来识别缺陷和错误。本文将探讨白盒测试中的静态分析技术及其在维护代码质量和提升测试效率中的应用,同时介绍相关工具和最佳实践,为读者提供一种系统化的白盒测试方法论。
17 3
|
1月前
|
Java 测试技术 数据库
springboot大学生体质测试管理系统
springboot大学生体质测试管理系统
|
3月前
|
测试技术 开发者 Python
Python自动化测试与单元测试框架:提升代码质量与效率
在软件开发过程中,测试是不可或缺的环节。Python作为一门广泛应用的编程语言,拥有丰富的自动化测试和单元测试框架,例如unittest和pytest。本文将介绍Python自动化测试的重要性,并深入探讨这两个主流的单元测试框架的特点、使用方法以及优势。通过学习和应用这些框架,开发者可以提高代码质量、提升开发效率,并确保软件在不断迭代中保持稳定。
|
3月前
|
前端开发 JavaScript API
React 生态系统:路由、状态管理、调试、测试、组件库、文档……
React 生态系统:路由、状态管理、调试、测试、组件库、文档……
41 0
|
3月前
|
监控 Java 测试技术
基于springboot实现的个人性格测试系统(分前后端)
基于springboot实现的个人性格测试系统(分前后端)

热门文章

最新文章