当前位置: 首页 > 科技观察

复杂系统中如何保证代码质量?先来测试

时间:2023-03-18 21:08:42 科技观察

业务背景高德的在线导航服务作为已有的系统,具有很强的业务特性和多年的历史,难免存在大量不合理的代码,业务演化对系统性能、算法、底层架构影响很大等不断提出更高的要求,现有的业务代码、算法与架构快速演进的需求冲突严重。如何有效保证质量,进行快速重构演进,成为业务发展面临的首要工程问题。二、现有质量保证方法存在的问题及分析1.现有测试方法存在的问题常规方法是对新旧业务进行批量比对。这种方法简单有效,也是我们一直在使用的方法,但是存在以下问题:Invaliddiff问题:以公交规划引擎为例,依赖于计步引擎等多个下游服务,搜索、公共汽车紧急情况和路况。获得的结果的差异导致许多无效的差异。运行时间长:案例数大时运行时间较长,在10分钟级别。由于这一步成本较高,一般开发者跑diff的频率不会太高,不可能进行“一小步一小步”的测试。排查困难:发现diff很难排查,因为是整个请求层面的diff,中间步骤可能会出问题。2业界主流方法实践ThoughtWorks、Google等公司采用TDD方法进行敏捷开发,通过编写单元测试用例来保证开发和重构的质量,已成为主流最佳实践。3单元测试简介1什么是单元测试?单元测试是检查模块、函数或类的正确性的测试。测试粒度更小更轻,运行时间秒级,特别适用于增量重构中“一步一个脚印”的质量保证。由于单元测试用例是针对函数或类的更细粒度的目标,当测试用例失败时,可以快速锁定问题点。2单元测试框架常见的单元测试框架有xUnit系列,有多种语言实现,如CppUnit、JUnit、NUnit……GTest是Google开发的单元测试框架。这个框架有一些高级功能,比如死亡测试,模拟等待。我们选择了GTest框架。3单元测试、重构、TDD与敏捷TDD(TestDrivenDevelopment)是强调测试先行的开发方法。这种方式的好处是,在编写任何功能和修改任何代码时,都可以通过编写单元测试用例代码来表达你想要实现的代码的功能,测试用例本身就是代码表达的一种需求。积累的测试用例可以有效保证开发和后续重构演化的质量。重构和TDD是敏捷方法的核心组成部分。没有TDD的敏捷是危险的。没有用例保证的重构一旦开始,就如脱缰的野马。单元测试和TDD是拴住野马的缰绳。四总线服务单元测试实践1GTest框架集成Git库地址:https://github.com/google/googletestGTest框架集成很简单,在项目中添加googletest库,添加链接libgtest:即可驱动以下代码用例执行:intRCUnitTest::Excute(){intargc=2;char*argv[]={const_cast(""),const_cast("--gtest_output=\"xml:./testAll.xml\"")};::testing::InitGoogleTest(&argc,argv);returnRUN_ALL_TESTS();开关控制:为避免影响正式版,可以考虑编译控制或增加配置项开关。我们在使用的时候,通过入口处的一个配置项来控制是否触发单元测试用例。编译时,默认只链接入口文件。当需要运行单元测试时,添加单元测试用例文件链接运行。2测试代码是通过实现Test类的派生类,然后使用TEST_F宏添加测试函数来编写的,如下例所示:classDateTimeUtilTest:public::testing::Test{protected:virtualvoidSetUp(){}virtualvoidTearDown(){}};TEST_F(DateTimeUtilTest,TestAddSeconds_leap){//闰年测试2020-02-28tmtt;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-28tmtt1;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);};测试用例执行效果:目前公交引擎已经积累了23个模块测试用例,基本涵盖了寻站、寻路、ETA、票价、风险停运等核心功能,还在不断积累中。以单元测试为保障,每个版本开发活动都在进行渐进式重构活动,有效保证质量,测试迭代次数和新上线代码引入的问题数量保持在较低水平。3问题与难点数据依赖在线导航引擎是一个对数据高度依赖的业务。多组数据结构相互关联,字段较多。没有数据很难构建有效的单元测试。通过mocking构造假数据的成本非常高。并且数据更改将导致用例失败。我的做法:那些容易构造假数据的,可以通过构造假数据来解决。对于难以构造假数据的情况,直接使用真实数据就足够了。数据更改可能会导致这些用例失败。没关系。你只需要确保在每次重构之前调用相关的用例,这样重构过程的质量仍然可以得到保证。即:不是要保证用例随时随地都可以运行,而是要保证重构前后都能通过。4常见的误区对于没有真正实践过单元测试和TDD开发方法的同学来说,在认知上存在一些常见的误区,比如:开发时间不够,哪有时间写单元测试?我的理解:首先,TDD开发方式强调测试在前,测试代码的编写在前。这个过程相当于理解需求的过程。也就是想想你要实现什么功能?这段测试代码是把需求弄清楚的产物,仅此而已,没有更多的时间成本。TDD开发方式是典型的一次性投资,持续受益。用例积累的越多,越容易在前期发现问题。重构质量有保障,代码越来越干净清晰。开发者再也不用哀叹历史代码了。历史代码那么多,单元测试怎么编?然后从添加第一个用例开始。我的做法是将本次修改涉及的代码对应的用例添加进去,逐步积累。添加用例的过程就是理解现有代码的过程。对于已有的历史代码、各种硬编码入侵、各种耦合、全局变量或生命周期长的大对象,通过编写单元测试用例,可以有效理清函数的真实输入输出,也为重构增加了有效保障。五股复杂系统代码增量重构对于我们一线码农来说,每天大部分时间都在和代码打交道。如果你维护的代码结构合理,易读易扩展,那么恭喜你!但我们面临的大多数情况是既有项目,有着各种历史“积累”,各种变化牵一发而动全身。在这种情况下,小的改动可以细心、细心地做,但是一些大的系统升级就很难做。向上。对于庞大的业务系统,重写在成本和质量控制上更不现实。然后设置几个大节点,通过逐步重构逐步优化,变量发生质变,综合来看是最好的方法。单元测试和TDD是增量重构有效实施的必要手段。