首頁>技術>

一 業務背景

高德線上導航服務作為有很強業務特性和多年曆史積累的存量系統,不可避免的存在大量的不合理程式碼,而業務演進對系統性能、演算法、底層架構等不斷提出更高要求,存量的各種業務程式碼和演算法、架構快速演進的訴求存在嚴重衝突,如何有效保障質量地進行快速重構式演進,成為業務發展面臨的首要工程難題。

二 現有質量保障方法問題與分析

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 即可:

透過如下程式碼即可驅動用例執行:

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);};

目前公交引擎已經積累了23個模組測試用例,基本覆蓋了尋站、尋路、ETA、票價、風險停運等核心功能,持續積累中。透過單元測試保障,每個版本開發活動中都在進行漸進式重構活動,能夠有效保障質量,提測迭代次數和線上新增程式碼引入問題數量持續較低。

3 問題與難點

資料依賴問題

線上導航引擎是對資料重度依賴的業務,多組資料結構之間互相關聯,欄位繁多,很難脫離資料構建有效的單元測試。透過mock方式構造假資料成本很高。而資料變化將導致用例不能透過。

我的實踐:

能夠簡單構造假資料的透過構造假資料來搞定。

對於很難構建假資料的情況,直接使用真實資料即可。資料變化可能導致這部分用例不透過,沒有關係,只需要保障在每次重構前把相關的用例調通即可,這樣仍可以確保重構過程的質量。即:不需要做到用例隨時隨地都能執行透過,而是保證重構前後都可以透過。

4 常見錯誤認知

對於沒有真正實踐過單元測試和TDD開發方式的同學來說,有一些認知上的常見誤區,比如:

開發時間都不夠, 哪有時間編寫單元測試?

我的理解:

首先TDD的開發方式強調的是測試先行,編寫測試程式碼是在前面的,這個過程等於是理解需求的過程。即想清楚你要實現的是什麼功能?這個測試程式碼是理清需求的產物, 如此而已,不存在更多時間成本。TDD開發方式屬於典型的一次投入,持續受益的事情,用例積累越多,越容易在早期發現問題,重構有了質量保障,程式碼越來越整潔清晰,開發同學們再也不用哀嘆歷史程式碼。

歷史程式碼那麼多,怎麼補單元測試?

那就從新增第一個用例開始。我的做法是對應本次修改涉及到的程式碼新增用例,逐步積累。

新增用例的過程是理解現有程式碼的過程,對於存量的歷史程式碼,各種硬性編碼侵入,各種耦合,全域性變數或長生命週期大物件,透過編寫單元測試用例能夠有效理清函式真正的輸入輸出,也為重構增加了有效保障。

五 存量複雜系統程式碼漸進式重構

對於我們一線碼農,每天大部分時間都在和程式碼打交道,如果你維護的程式碼結構合理、易讀易擴充套件,那麼恭喜你!但大部分情況我們面對的是存在各種歷史"積澱"的存量工程,各種牽一髮而動全身,這種情況下小改動還可以靠多花時間,認真仔細來搞定,但想要做一些大的系統升級就難了。

而對於巨型業務系統來說,重寫在成本和質量控制方面顯得更不現實。那麼設定幾個大的節點,透過漸進式重構逐漸最佳化,變數變為質變,是綜合來看最優的方式。

而單元測試和TDD,則是漸進式重構有效開展的必選方法。

8
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 計算機視覺四大基本任務(分類、定位、檢測、分割)