新聞中心
一 業(yè)務(wù)背景

高德在線導(dǎo)航服務(wù)作為有很強(qiáng)業(yè)務(wù)特性和多年歷史積累的存量系統(tǒng),不可避免的存在大量的不合理代碼,而業(yè)務(wù)演進(jìn)對(duì)系統(tǒng)性能、算法、底層架構(gòu)等不斷提出更高要求,存量的各種業(yè)務(wù)代碼和算法、架構(gòu)快速演進(jìn)的訴求存在嚴(yán)重沖突,如何有效保障質(zhì)量地進(jìn)行快速重構(gòu)式演進(jìn),成為業(yè)務(wù)發(fā)展面臨的首要工程難題。
二 現(xiàn)有質(zhì)量保障方法問(wèn)題與分析
1 現(xiàn)有測(cè)試方法的問(wèn)題
常規(guī)方法是對(duì)新老服務(wù)批量進(jìn)行請(qǐng)求比較diff,這種方式簡(jiǎn)單有效,是我們一直在用的方法,但存在以下問(wèn)題:
- 無(wú)效diff問(wèn)題:以公交規(guī)劃引擎為例,依賴步導(dǎo)引擎、搜索、公交突發(fā)事件、路況等多個(gè)下游服務(wù),獲取結(jié)果的差異導(dǎo)致很多無(wú)效diff。
- 運(yùn)行時(shí)間較長(zhǎng):case量較多時(shí)運(yùn)行時(shí)間較長(zhǎng),在10分鐘級(jí)別。由于這一步成本較高,一般開發(fā)人員跑diff的頻率不會(huì)太高,無(wú)法進(jìn)行"每次一小步"的測(cè)試。
- 排查困難:當(dāng)發(fā)現(xiàn)diff后進(jìn)行排查非常困難,因?yàn)槭钦麄€(gè)請(qǐng)求級(jí)別的diff,中間步驟可能都存在問(wèn)題。
2 業(yè)界主流方法實(shí)踐
ThoughtWorks、Google等公司使用TDD方式進(jìn)行敏捷開發(fā),通過(guò)編寫單元測(cè)試用例保障開發(fā)、重構(gòu)的質(zhì)量,目前已經(jīng)成為主流最佳實(shí)踐。
三 單元測(cè)試介紹
1 什么是單元測(cè)試?
單元測(cè)試是對(duì)一個(gè)模塊、一個(gè)函數(shù)或者一個(gè)類進(jìn)行正確性檢驗(yàn)的測(cè)試工作。
測(cè)試的粒度更小更輕量,運(yùn)行時(shí)間在秒級(jí),特別適合漸進(jìn)式重構(gòu)中的"每次一小步"的質(zhì)量保障。
由于單元測(cè)試用例針對(duì)的是一個(gè)函數(shù)、類更細(xì)粒度的目標(biāo),所以當(dāng)某個(gè)用例不通過(guò)時(shí),可以快速鎖定問(wèn)題點(diǎn)。
2 單元測(cè)試框架
常見(jiàn)單元測(cè)試框架有 xUnit 系列,多種語(yǔ)言都有對(duì)應(yīng)實(shí)現(xiàn),如CppUnit、JUnit、NUnit...
GTest是Google開發(fā)的單元測(cè)試框架,此框架具有一些高級(jí)功能,如death test, mock等。
我們選擇的是GTest框架。
3 單元測(cè)試、重構(gòu)、TDD與敏捷
TDD(Test Driven Development)是強(qiáng)調(diào)測(cè)試先行的開發(fā)方式,這種方式的好處在于編寫任何函數(shù)、修改任何代碼時(shí)可以通過(guò)編寫一個(gè)單元測(cè)試用例代碼來(lái)表達(dá)要實(shí)現(xiàn)的代碼功能,一個(gè)測(cè)試用例本身就是一個(gè)代碼表達(dá)的需求。而積累起來(lái)的測(cè)試用例可以有效保障開發(fā)及后續(xù)重構(gòu)演進(jìn)的質(zhì)量。
重構(gòu)和TDD是敏捷方法的核心構(gòu)成要素,脫離了TDD的敏捷是危險(xiǎn)的,沒(méi)有用例保障的重構(gòu)一旦啟動(dòng),就像一匹脫韁的野馬。而單元測(cè)試和TDD則是縛住野馬的韁繩。
四 公交服務(wù)單元測(cè)試實(shí)踐
1 GTest框架集成
Git庫(kù)地址:https://github.com/google/googletest
GTest框架集成非常簡(jiǎn)單,把googletest庫(kù)加入到工程中, 增加鏈接 libgtest 即可:
通過(guò)如下代碼即可驅(qū)動(dòng)用例執(zhí)行:
- int RCUnitTest::Excute()
- {
- int argc = 2;
- char* argv[] = {const_cast
(""), const_cast ("--gtest_output=\"xml:./testAll.xml\"")}; - ::testing::InitGoogleTest(&argc, argv);
- return RUN_ALL_TESTS();
開關(guān)控制:為避免影響到正式版本, 可以考慮通過(guò)編譯控制,也可以增加一個(gè)配置項(xiàng)開關(guān)。
我們?cè)谑褂脮r(shí)是在入口處通過(guò)一個(gè)配置項(xiàng)控制是否觸發(fā)單元測(cè)試用例,編譯時(shí)默認(rèn)只鏈接入口文件,需要運(yùn)行單元測(cè)試時(shí)添加上單元測(cè)試用例文件進(jìn)行鏈接運(yùn)行。
2 測(cè)試代碼編寫
通過(guò)實(shí)現(xiàn)一個(gè)Test類的派生類,然后使用TEST_F宏添加測(cè)試函數(shù)即可,如下示例:
- class DateTimeUtilTest : public ::testing::Test
- {
- protected:
- virtual void SetUp()
- {
- }
- virtual void TearDown()
- {
- }
- };
- TEST_F(DateTimeUtilTest, TestAddSeconds_leap)
- {
- //閏年測(cè)試 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);
- //非閏年測(cè)試 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);
- };
測(cè)試用例執(zhí)行效果:
目前公交引擎已經(jīng)積累了23個(gè)模塊測(cè)試用例,基本覆蓋了尋站、尋路、ETA、票價(jià)、風(fēng)險(xiǎn)停運(yùn)等核心功能,持續(xù)積累中。通過(guò)單元測(cè)試保障,每個(gè)版本開發(fā)活動(dòng)中都在進(jìn)行漸進(jìn)式重構(gòu)活動(dòng),能夠有效保障質(zhì)量,提測(cè)迭代次數(shù)和線上新增代碼引入問(wèn)題數(shù)量持續(xù)較低。
3 問(wèn)題與難點(diǎn)
數(shù)據(jù)依賴問(wèn)題
在線導(dǎo)航引擎是對(duì)數(shù)據(jù)重度依賴的業(yè)務(wù),多組數(shù)據(jù)結(jié)構(gòu)之間互相關(guān)聯(lián),字段繁多,很難脫離數(shù)據(jù)構(gòu)建有效的單元測(cè)試。通過(guò)mock方式構(gòu)造假數(shù)據(jù)成本很高。而數(shù)據(jù)變化將導(dǎo)致用例不能通過(guò)。
我的實(shí)踐:
能夠簡(jiǎn)單構(gòu)造假數(shù)據(jù)的通過(guò)構(gòu)造假數(shù)據(jù)來(lái)搞定。
對(duì)于很難構(gòu)建假數(shù)據(jù)的情況,直接使用真實(shí)數(shù)據(jù)即可。數(shù)據(jù)變化可能導(dǎo)致這部分用例不通過(guò),沒(méi)有關(guān)系,只需要保障在每次重構(gòu)前把相關(guān)的用例調(diào)通即可,這樣仍可以確保重構(gòu)過(guò)程的質(zhì)量。即:不需要做到用例隨時(shí)隨地都能運(yùn)行通過(guò),而是保證重構(gòu)前后都可以通過(guò)。
4 常見(jiàn)錯(cuò)誤認(rèn)知
對(duì)于沒(méi)有真正實(shí)踐過(guò)單元測(cè)試和TDD開發(fā)方式的同學(xué)來(lái)說(shuō),有一些認(rèn)知上的常見(jiàn)誤區(qū),比如:
開發(fā)時(shí)間都不夠, 哪有時(shí)間編寫單元測(cè)試?
我的理解:
- 首先TDD的開發(fā)方式強(qiáng)調(diào)的是測(cè)試先行,編寫測(cè)試代碼是在前面的,這個(gè)過(guò)程等于是理解需求的過(guò)程。即想清楚你要實(shí)現(xiàn)的是什么功能?這個(gè)測(cè)試代碼是理清需求的產(chǎn)物, 如此而已,不存在更多時(shí)間成本。
- TDD開發(fā)方式屬于典型的一次投入,持續(xù)受益的事情,用例積累越多,越容易在早期發(fā)現(xiàn)問(wèn)題,重構(gòu)有了質(zhì)量保障,代碼越來(lái)越整潔清晰,開發(fā)同學(xué)們?cè)僖膊挥冒@歷史代碼。
歷史代碼那么多,怎么補(bǔ)單元測(cè)試?
那就從添加第一個(gè)用例開始。我的做法是對(duì)應(yīng)本次修改涉及到的代碼添加用例,逐步積累。
添加用例的過(guò)程是理解現(xiàn)有代碼的過(guò)程,對(duì)于存量的歷史代碼,各種硬性編碼侵入,各種耦合,全局變量或長(zhǎng)生命周期大對(duì)象,通過(guò)編寫單元測(cè)試用例能夠有效理清函數(shù)真正的輸入輸出,也為重構(gòu)增加了有效保障。
五 存量復(fù)雜系統(tǒng)代碼漸進(jìn)式重構(gòu)
對(duì)于我們一線碼農(nóng),每天大部分時(shí)間都在和代碼打交道,如果你維護(hù)的代碼結(jié)構(gòu)合理、易讀易擴(kuò)展,那么恭喜你!但大部分情況我們面對(duì)的是存在各種歷史"積淀"的存量工程,各種牽一發(fā)而動(dòng)全身,這種情況下小改動(dòng)還可以靠多花時(shí)間,認(rèn)真仔細(xì)來(lái)搞定,但想要做一些大的系統(tǒng)升級(jí)就難了。
而對(duì)于巨型業(yè)務(wù)系統(tǒng)來(lái)說(shuō),重寫在成本和質(zhì)量控制方面顯得更不現(xiàn)實(shí)。那么設(shè)置幾個(gè)大的節(jié)點(diǎn),通過(guò)漸進(jìn)式重構(gòu)逐漸優(yōu)化,變量變?yōu)橘|(zhì)變,是綜合來(lái)看最優(yōu)的方式。
而單元測(cè)試和TDD,則是漸進(jìn)式重構(gòu)有效開展的必選方法。
新聞標(biāo)題:復(fù)雜系統(tǒng)如何保障代碼質(zhì)量?讓測(cè)試先行
文章轉(zhuǎn)載:http://fisionsoft.com.cn/article/cdggddg.html


咨詢
建站咨詢
