午夜视频在线观看区二区-午夜视频在线观看视频-午夜视频在线观看视频在线观看-午夜视频在线观看完整高清在线-午夜视频在线观看网站-午夜视频在线观看亚洲天堂

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

單元測試從入門到精通

freeflydom
2025年3月10日 16:5 本文熱度 358

1 前言

這篇文章源于工作中的一個項目,2021年,我負責匯川技術工業機器人應用軟件的基礎架構重構,當時單元測試是重構工作的核心環節之一,從無法進行單元測試到最終60%以上的行覆蓋率,過程中自己也有非常多的收獲,于是將其整理成文,希望對計劃開展和正在開展單元測試的同學有所幫助。

2 什么是單元測試

 

單元測試(Unit Testing),是指對軟件中的邏輯單元或組件進行檢查和驗證,以確保其按預期執行。通常單元測試是軟件開發過程中進行的最低級別測試活動,通過單元測試可發現和修復軟件開發早期的BUG和缺陷。

單元(Unit),是一個應用程序中最小的可測試部分,在面向過程開發中,單元通常為函數(Function),在面向對象開發中,單元通常為類中的方法(Method)。

3 為什么要進行單元測試

 

3.1 降低代碼缺陷

 

單元測試的首要目標是降低代碼缺陷,如上圖所示,當代碼缺陷越早被發現,它的修復成本就越低,這種把測試盡量提前進行的思想就叫做測試左移。

3.2 推動架構優化

 

單元測試與軟件架構有著非常緊密的聯系,通常越是架構設計優秀的項目(比如符合SOLID規則),越容易實施單元測試,反之越是架構糟糕的項目,越難以實施單元測試。并且單元測試以及其嚴格的方式要求軟件架構設計,如果架構設計存在問題,單元測試就根本無法開展。

假如你發現在自己的項目中實施單元測試舉步為艱,那么首先應該停下來觀察和思考一下,項目架構設計的是否合理?比如一些項目中UI與業務邏輯耦合,單元測試無法命中核心業務邏輯,可思考一下項目是否需要分層設計?UI與業務邏輯是否需要分離?比如項目中當碰到物理設備的依賴,單元測試就被阻斷,可思考一下是否永遠只使用這一臺設備?后續有沒有可能換成其它設備?是否需要考慮擴展性?再比如發現被測對象就是鐵板一塊,根本不能改變其協作對象的行為和數據,那么就應思考一下,對象之間是否存在強耦合?是否可以通過依賴注入降低和消除對象之間的耦合?

大多數情況我們在項目中實施單元測試的目的是為了保障代碼質量,但我認為單元測試對軟件架構優化的驅動實際更為重要。

3.3 守護代碼迭代質量

 

在當下的商業環境中,大魚吃小魚,快魚吃慢魚,對軟件開發的效率要求越來越高,傳統的瀑布開發模式越來越少,敏捷開發模式越來越普及。因此軟件版本快速迭代,快速測試,快速發布,小步快跑在大多數項目中成為常態。

而單元測試在代碼快速迭代過程中發揮著守護代碼質量的至關重要作用。當單元測試覆蓋了一個模塊中的業務邏輯,該業務邏輯在迭代變更過程中出現任何問題,會第一時間自動被單元測試捕獲,因此單元測試對發生變更的代碼正確性提供了保障,同時開發人員在這樣的保障下可以大膽的對代碼進行重構,對業務邏輯進行增減變更調整。大名鼎鼎的TDD(測試驅動開發)就是基于這個原理。

4 如何進行單元測試

 

4.1 使用AAA規則編寫測試用例

我們用一個簡單的示例來演示單元測試的編碼過程,如下代碼所示,是一個非常簡單的方法,它根據不同的距離,推薦不同的交通工具:

class UnitTestDemo
{
    public:
    // 一個被測方法,根據距離推薦交通工具
    // distance參數:距離(單位為千米)
    string StransportForDistance(float distance)
    {
        // 100公里內推薦的士  
        if (distance <= 100)  
            return "的士";  
        // 1000公里內推薦高鐵  
        if (distance <= 1000)  
            return "高鐵";  
        // 大于1000公里推薦飛機  
        return "飛機";  
    }  
}  

然后我們為這個方法編寫單元測試用例,它同樣非常的簡單:設定條件,調用被測方法,斷言返回結果:

// 一個單元測試用例
TEST(UnitTestDemo, TransportForDistance_Texi)
{
    // 設置初始條件(Arrange)
    UnitTestDemo unitTestDemo;
    float distance = 30;
    // 執行業務邏輯(Act)
    string s = unitTestDemo.TransportForDistance(distance);
    // 斷言測試結果(Assert)
    EXPECT_EQ(s, "的士");
} 

至此,單元測試的編碼就完成了。是的,單元測試的編碼已全部完成了,花30秒看懂這個示例,你就掌握了單元測試的核心方法。為了方便記憶,有人將它總結成了AAA規則:

Arrange

設置條件

Act

執行邏輯

Assert

斷言結果

4.2 讓每個測試用例符合AIR特性

AAA規則告訴我們如何編寫單元測試用例,但要編寫一個合格的單元測試用例,就需要了解單元測試用例的基本特性,這些特性就像空氣(AIR)一樣重要,任何時候我們也不能離開:

Automatic

自動化:單元測試應自動執行,而無需任何交互,測試用例通常被定期執行。

Independent

獨立性:每個單元測試用例都是獨立的個體,不允許測試用例之間存在依賴關系,也不允許要求測試用例被執行的先后順序。

Repeatable

可重復:單元測試用例在被重復執行時應穩定的返回相同的結果,不能受外部環境的影響。

4.3 在需要的時候使用測試替身

 

什么是測試替身

比如業務中我們的代碼與硬件設備連接,需要依賴硬件的不同狀態來執行不同的邏輯。單元測試的特性是隨時可重復執行,對硬件的依賴會阻塞單元測試執行,因此在單元測試用例中需要用一個“替身”替換掉硬件的狀態,這個“替身”就叫做測試替身。

測試替身的應用場景

1、真實對象具有不可確定的行為,或產生不可預測的結果。

2、真實對象很難被創建或創建成本過大,比如第三方系統、與硬件設備關聯的模塊。

3、真實對象的某些行為很難觸發,比如異常的觸發。

4、真實對象令測試用例的執行速度很慢。

5、真實對象有含有人機交互界面。

4.4 了解單元測試覆蓋方式

 

語句覆蓋

語句覆蓋又稱為行覆蓋,是單元測試中最簡單也是最常見的覆蓋率統計方式。被測函數中,只要被單元測試用例執行到的行,即認為該行被覆蓋到。比如一個100行的函數,其中有60行被單元測試用例執行到,那么語句覆蓋率為60%。

分支覆蓋

分支覆蓋又稱為判定覆蓋,它關注的是被測函數中產生分支的if判定結果,只要每個if語句判定為真和判定為假的分支都被執行到,即達成了分支覆蓋。注意分支覆蓋并不考慮多個分支間的組合關系。

條件覆蓋

條件覆蓋關注的是判定語句中的每個表達式是否被執行。比如判定語句 if (a() || b()) ,當a()返回為真時就不再執行b()了,此時就未達成條件覆蓋;要達成條件覆蓋,就需要使a()返回假。

路徑覆蓋

路徑覆蓋是單元測試中覆蓋最全的一種方式,它要求覆蓋被測試方法中所有邏輯分支路徑的組合。

總結

語句覆蓋在單元測試覆蓋率統計中最為常見,基本是一個必選項,分支覆蓋與條件覆蓋可作為進階選擇,路徑覆蓋最為完善,但是在復雜的業務場景中,會導致單元測試代碼指數級增長。建議根據實際情況靈活組合搭配。

4.5 單元測試過程中的一些常見疑問

 

由誰來編寫單元測試用例?

應該由開發人員編寫自己開發的功能對應的單元測試用例。有些項目中會安排專人來為其它人開發的功能編寫單元測試用例,這樣做效率很低,因為單元測試用例的編寫人員需要花費時間了解和學習代碼邏輯。

什么時間節點寫單元測試用例?

通常應該在功能開發完成后即編寫與之對應的單元測試用例,即使有延遲也不要延遲太長時間,時間過長會導致編寫單元測試用例時需要重新回顧代碼邏輯所帶來的額外時間成本。

單元測試是白盒還是黑盒測試?

絕大部分的單元測試是白盒測試,會根據函數中的邏輯設計編寫測試用例,以達到覆蓋率目標。但單元測試也可以是黑盒測試,比如一些API接口只關注輸入與輸出而不關注內部的邏輯實現。

5 可測試性設計

 

5.1 分層設計

 

將一件復雜的事情進行分解,是提升效率的基本手段,這在日常生活中非常常見。比如汽車的生產過程離散在多個零部件生產線,最后完成組裝。軟件中的分層設計,也是最常見的一種架構模式,在流行的開發框架中隨處可見。分層設計可以幫助單元測試準確的命中目標,比如通常情況下我們并不需要對UI而只希望對核心的業務邏輯進行單元測試,如果沒有分層,UI與業務邏輯耦合,就會使單元測試無法準確命中目標甚至寸步難行。

5.2 抽象設計

 

抽象是增強軟件擴展性的一把利劍,主板廠商很早就把抽象應用自如了。比如主板上的USB接口,并不針對某一種具體的設備,而只定義了USB標準:接口尺寸、電流、電壓、數據傳輸協議等,然后依據這個標準生產主板。USB標準即抽象,主板廠商通過抽象獲得了對無限種類USB設備的擴展支持。

因為有了USB標準,所以很容易就可以設計生產一個USB測試工裝,這個工裝就類似于單元測試中的測試替身,主板廠商在測試時,并不需要外接一個用戶經常使用的U盤或USB鍵盤,而只需要外接一個USB測試工裝即可完成測試,并且這個測試工裝可以在符合USB基本標準的前提下按測試需求設計生產,比如只需要按數據傳輸標準接收數據即可而并不需要真正的存儲數據。

5.3 依賴注入

 

當發生火警時,消防通道的暢通保障了救援。在單元測試中,依賴注入保障了代碼的可測試。由此可見,依賴注入在可測試性編碼中的重要性。

// 上帝視角
上帝造人()
{
    自己捏腦袋();
    自己捏胳膊();
    自己捏腿();
    ……
}
// 依賴注入
上帝造人( 腦袋, 胳膊, 腿 ……)
{
    組裝腦袋();
    組裝胳膊();
    組裝腿();
    ……
} 

如果所有職業按成就感進行排名的話,我想軟件開發一定是名列前茅的,因為大多數時候軟件開發人員扮演的就是“上帝角色”,他們可以隨時new一切需要的對象。但在依賴注入模式下,上帝需要從“自己創造”轉變為“習慣組裝”。

6 可測試性編碼

Google的研發工程師寫了一篇關于軟件可測試性的文章《Guide: Writing Testable Code》,覺得里面的代碼示例比較具有代表性,摘錄并整理簡化了代碼(可不關注語法細節,當作偽代碼來看)如下:

6.1 注入協作對象

難以測試的代碼示例:

// 被測對象
public class House
{
    private Bedroom bedroom;
    House() 
    { 
        // 在類的構造函數中構造協作對象,可測試性差。
        bedroom = new Bedroom(); 
    }
    // ...
}
// 測試用例
public void TestThisIsReallyHard()
{
    House house = new House();
    // 無法控制Bedroom對象,難以測試
    // ...
} 

易于測試的代碼示例:

// 被測對象
public class House
{
    private Bedroom bedroom;
    // 注入協作對象,可測試性好。
    House(Bedroom b) 
    { 
        bedroom = b;
    }
    // ...
}
// 測試用例
public void TestThisIsEasyAndFlexible()
{
    // Bedroom對象在掌控之中,易于測試
    Bedroom bedroom = new Bedroom();
    House house = new House(bedroom);
    // ...
} 

6.2 不要依賴靜態方法

難以測試的代碼示例:

// 被測對象
public class TrainSchedules 
{ 
    Schedule FindNextTrain() 
    { 
        // 與靜態方法強耦合,難以測試
        if (TrackStatusChecker.IsClosed(track)) 
        { 
            // ...
        } 
        // ... return a Schedule
    } 
}
// 測試用例
public void TestFindNextTrainNoClosings()
{
    // 靜態方法出現長耗時,阻塞單元測試
    AssertNotNull(schedules.FindNextTrain());
}

易于測試的代碼示例:

// 將靜態方法包裝在一個注入類中,并將其抽象(設計實現IStatusChecker接口)
public class TrackStatusCheckerWrapper : IStatusChecker
{ 
    public bool IsClosed(Track track) 
    { 
        return TrackStatusChecker.IsClosed(track); 
    } 
} 
// 被測對象
public class TrainSchedules 
{ 
    private StatusChecker wrappedLibrary; 
    // 1、通過依賴注入解除與靜態方法之間的強耦合
    // 2、支持通過抽象使用測試替身,消除長耗時
    public TrainSchedules(IStatusChecker wrappedLibrary) 
    { 
        this.wrappedLibrary = wrappedLibrary; 
    } 
    public Schedule FindNextTrain() 
    { 
        if (wrappedLibrary.IsClosed(track)) 
        { 
            // ... 
        } 
        // ... return a Schedule 
    } 
}
// 測試用例
public void TestFindNextTrainNoClosings() 
{
    // 支持通過抽象替換掉靜態方法中的耗時邏輯
    IStatusChecker localWrapper = new StubStatusCheckerWrapper();
    TrainSchedules schedules = new TrainSchedules(localWrapper);
    AssertNotNull(schedules.FindNextTrain()); 
} 

6.3 不要依賴全局變量

難以測試的代碼示例:

// 被測對象
public class NetworkLoadCalculator
{
    public int CalculateTotalLoad()
    {
        // 依賴全局變量,難以測試
        string algorithm = ConfigFlags.FLAG_loadAlgorithm.Get();
        // ...
    }
}
// 測試用例
public void TestMaximumAlgorithmReturnsHighestLoad() 
{ 
    // 缺陷1:一旦忘了復原全局變量就會導致后續其它測試用例執行失敗
    // 缺陷2:全局變量導致測試用例無法并行執行
    // 設置全局變量
    ConfigFlags.FLAG_loadAlgorithm.SetForTest("maximum"); 
    NetworkLoadCalculator calc = new NetworkLoadCalculator(); 
    calc.SetLoadSources(10, 5, 0); 
    AssertEquals(10, calc.CalculateTotalLoad()); 
    // 復原全局變量
    ConfigFlags.FLAG_loadAlgorithm.ResetForTest(); 
} 

易于測試的代碼示例:

// 被測對象
public class NetworkLoadCalculator 
{ 
    private string loadAlgorithm; 
    // 使用依賴注入,可測試性好
    NetworkLoadCalculator(string loadAlgorithm) 
    { 
        this.loadAlgorithm = loadAlgorithm; 
    } 
    // ... 
}
// 測試用例
public void TestMaximumAlgorithmReturnsHighestLoad() 
{
    // 不再依賴全局變量,解除了測試用例之間相互影響的風險
    NetworkLoadCalculator calc = new NetworkLoadCalculator("maximum"); 
    calc.SetLoadSources(10, 5, 0); 
    AssertEquals(10, calc.CalculateTotalLoad()); 
} 

6.4 不要為了測試而測試

難以測試的代碼示例:

// 協作對象
public class VideoPlaylistIndex
{
    private VideoRepository repo;
    // 單元測試專屬構造函數,業務中不使用
    public VisibleForTesting VideoPlaylistIndex (VideoRepository repo)
    {
        this.repo = repo;
    }
    // 業務中使用的構造函數
    public VideoPlaylistIndex()
    {
        // 執行緩慢的邏輯
        this.repo = new FullLibraryIndex();
    }
}
// 被測對象
public class PlaylistGenerator
{
    private VideoPlaylistIndex index = new VideoPlaylistIndex();
    public Playlist buildPlaylist(Query q)
    {
        return index.search(q);
    }
}
// 測試用例
public void TestBadDesignHasNoSeams()
{
    // 雖然VideoPlaylistIndex容易進行測試,但PlaylistGenerator卻難以測試,執行緩慢的構造函數無法被替換
    PlaylistGenerator generator = new PlaylistGenerator();
} 

易于測試的代碼示例:

// 協作對象
public class VideoPlaylistIndex
{
    private VideoRepository repo;
    // 業務與單元測試共用構造函數
    public VisibleForTesting VideoPlaylistIndex (VideoRepository repo)
    {
        this.repo = repo;
    }
}
// 被測對象
public class PlaylistGenerator
{
    private VideoPlaylistIndex index;
    // 使用依賴注入
    public PlaylistGenerator(VideoPlaylistIndex index)
    {
        this.index = index;
    }
    public Playlist buildPlaylist(Query q)
    {
        return index.search(q);
    }
}
// 測試用例
public void TestFlexibleDesignWithDI()
{
    // 通過依賴注入替換掉可能耗時的操作
    VideoPlaylistIndex fakeIndex = new InMemoryVideoPlaylistIndex();
    PlaylistGenerator generator = new PlaylistGenerator(fakeIndex);
} 

7 使用測試框架

 

7.1 GTest簡介

測試框架為我們提供了測試用例管理、斷言、參數化、用例執行等系列通用功能,使我們可以專注于測試用例本身業務邏輯的處理。在C/C++編程中,GTest當前最流行的單元測試框架,它由Google公司發布,支持跨平臺(Linux、Windows、MacOS),GTest官方倉庫地址為:https://github.com/google/googletest

7.2 使用GTest編寫單元測試用例

// 一個簡單的單元測試用例示例
TEST(Test_Suite_Name, Test_Case_Name)
{
    // 設置初始條件
    // ...
    // 調用被測試方法
    // ..
    // 斷言測試結果
    EXPECT_EQ(varString, "Assert Result");
} 

GTest框架會自動執行所有單元測試用例(由TEST、TEST_F等宏定義),一個單元測試用例類似于一個函數,其中第一個參數為測試套件名稱,測試套件就是一系列單元測試用例的集合,第二個參數為單元測試用例名稱,如上代碼所示。

// 自定義一個測試套件
class Test_Suite : public ::testing::Test
{
protected:
    // 全局初始化(所有測試用例執行前)
    static void SetUpTestCase()
    {}
    // 全局清理(所有測試用例執行后)
    static void TearDownTestCase()
    {}
    // 測試用例初始化(單個測試用例執行前)
    void SetUp() override
    {}
    // 測試用例清理(單個測試用例執行后)
    void TearDown() override
    {}
    // 其它公共資源
    // ...
}
// 使用自定義測試套件的單元測試用例
TEST_F(Test_Suite, Test_Case_Name)
{
    // 設置初始條件
    // ...
    // 調用被測方法
    // ..
    // 斷言測試結果
    EXPECT_TRUE(varBool);
} 

在實際項目中,通常相同類型的多個測試用例需要相同的初始化和清理過程,或需要共用一些資源。此時就可以使用自定義測試套件方式,如上代碼所示。

7.3 單元測試覆蓋率統計

單元測試覆蓋率通常指的是行覆蓋率,其計算規則為:分母為被測項目有效代碼(排除空白、注釋等無效行)的總行數,分子為被單元測試用例執行到的行數,由此計算的比例為單元測試行覆蓋率。華為大多軟件項目對外宣稱的單元測試行覆蓋率為70%,根據我的經驗,這是一個相當高的比例了。

有很多統計單元測試覆蓋率的工具,比如針對C++的 OpenCppCoverage ,安裝后通過一條命令即可生成HTML可視化的單元測試覆蓋率統計報表:

OpenCppCoverage.exe --source 待分析的源代碼目錄 -- 單元測試項目生成的.exe

8 單元測試的成敗關鍵

 

8.1 時間與成本預算

決定在項目中實施單元測試前,需要與項目經理充分溝通項目時間周期與成本,因為單元測試需要增加開發工程師在編碼階段的時間投入,這個比例大致在0.5~1.0之間。即假如某個功能的編碼時間是10天,那么需要增加大約5-10天來完成單元測試。同時單元測試并非一勞永逸,后續當被測試的業務代碼發生變更,與之對應的單元測試用例也需要同步變更。因此獲得相應的項目資源預算對單元測試的成敗至關重要,如果沒有給到開發人員相對充裕的時間,但又要求他們達成單元測試指標,就會導致開發人員認為單元測試擠占了功能開發時間,從而排斥單元測試。

8.2 在軟件架構設計階段整體考慮可測試性(架構師)

 

可測試性架構設計是達成單元測試在技術層面最重要的環節,好比房屋裝修,如果軟裝都完成了,冰箱彩電空調擺放就位,才發現忘了走電源線,那么補救成本就非常高了。

8.3 在編碼階段具備可測試性意識(開發工程師)

除了架構設計提前考慮對單元測試的支持,軟件編碼亦是如此,開發人員在編寫代碼前應提前了解單元測試,以不至于編寫出來的代碼不能或難以進行單元測試。比如全局變量滿天飛導致測試用例之間相互影響,類中的協作對象完全不使用依賴注入導致測試用例無從下手,等等。

9 后記

本文介紹了單元測試的基本概念,以及結合實際項目,分享了單元測試實施要點。是對自己項目過程的總結,也希望對有需要的同學有所幫助。

最后做一點補充,實施單元測試大致分為兩類,我稱之為主動單元測試和被動單元測試,主動單元測試,是以提升代碼質量和軟件架構為目的,由內部主動發起,實施過程中會同步優化軟件架構、提升代碼可測試性。而被動單元測試由外部驅使,比如來自客戶或市場的外部要求,它以覆蓋率為唯一目標,通常會借助一些商業工具(比如Tessy),自動生成單元測試用例與完成打樁,它不需要修改源程序代碼,當然也不會提升軟件的架構質量。本文所描述的,以及我個人比較推崇的為主動單元測試。

<全文完>

?轉自https://www.cnblogs.com/wubayue/p/18760269


該文章在 2025/3/10 16:06:07 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

主站蜘蛛池模板: 国产尤物在线观看无码不卡 | 成人午夜精品无码区 | 国产精品人人做人人爽 | 国产av寂寞骚妇 | 国产一级av毛片国语对白 | 国产av一区二区三区最新精品 | 国产精品日韩无卡一区二区 | 国内无码三级v观看 | 国产三级观看 | 精品国产丝袜黑色高跟鞋 | 国产精品丝袜久久久久久不卡 | 国产在线自在拍91有声 | 成人午夜影院在线观看 | 国产精品亚洲精品青青青 | 97无码人妻视频在线 | 国产高清中文精品 | 国产精品无码1区2区3区 | a级日本乱理伦片免费入口 a级日本乱理伦片免费入口: | 国产成人综合日韩精 | 99国产欧美久久 | 国产欧美日韩综合aⅴ天堂 国产欧美日韩综合第一区第二区 | 国产精品日韩欧美制服 | 18禁动漫美女禁处被爆桶出水 | 国产精品v毛片免费看观看 国产精品v欧美 | 国产大神高清视频在线观看 | 国产视频一区二区在线观看 | 国产凹凸在线一区二区 | 国产对白刺激 | 国产精品边做奶水狂喷 | 97人妻中文字幕免费视频 | 91黄视频在线 | 国产小视频免费在线观看 | 97人妻碰碰碰久久久久禁片 | 2025亚洲精品无码在钱 | 国产精品女av片爽免费观看 | 国产成人无码a区在线视频 国产成人无码mv精品 | 91久久精品在这里色伊人68 | 国产毛片一区二区三区视频 | 国产色视频一区二区三区不卡 | 成人国产一区二区三区久久久 | 91精选国产免费高清 |