1. 什麼是單元測試?
單元測試,維基百科給出定義:Unit Testing,又稱爲模塊測試,是針對程序模塊(軟件設計的最小單元)進行正確性檢驗的測試工作。
2. 什麼是模塊?或者什麼是最小單元?
通俗的說就是函數或者類的方法。
“單元”的定義,其實可以更加寬泛,在面嚮對象語言中,一個單元可以指一個方法,也可以是一個類。單元的選定更多的取決於我們測試的意圖。
3. 爲什麼需要單元測試?
我們常說的單元測試,是開發者編寫的一小段代碼,用於檢驗被測代碼的一個很小的、很明確的功能是否正確。簡單來說,一個單元測試就是用於判斷某個特定條件(或者場景)下某個特定函數的行爲。
好處:開發人員可以通過這種方式,測試自己寫的函數是否符合預期,在這個過程中,往往可以發現函數內部邏輯錯誤,帶來優秀的代碼治理和良好的異常處理以及完善錯誤報告。
4. 怎麼寫單元測試?或者怎麼纔算好的單元測試?
有明確的預期;
可重複運行;
沒有副作用。
這裏說到的副作用,即業務邏輯從外部依賴中解耦處理,比如說:時間、隨機數、併發性、基礎設置、持久化、網絡等。這就需要我們對代碼做更合理的劃分,函數的職責更加的清晰。
但我們的往往有各種外部依賴,比如需要讀寫數據庫,需要進行網絡通信,需要操作設備等等,這就需要用到測試替身:Stub(樁)。樁的最大作用就是,不包含邏輯,返回固定數據。
5. 這裏主要是記錄一下C++的gmock的使用。
雖然gmock的入門看起來很簡單,但是將其應用到具體的工程中,初學者又總會面臨各種各樣的問題,主要原因還是對於gmock是如何運作的存在誤區,下面通過一個例子來看下,究竟gmock是如何替換某些函數方法的。
1)首先需要在C++工程中,設置附加包含目錄和附加庫目錄爲gtest和gmock的include和lib路徑,附加依賴項中添加:gmock.lib和gtest.lib。需要注意的是,所使用的gmock.lib和gtest.lib與你所創建的C++工程,最好是一樣的vs版本,即C++庫版本一樣,否則會出現一些意想不到的錯誤哦。
2)這裏有一個test類,先假定它的作用就是沒啥作用,就是給你看下:
正常來說,我們一般會這樣寫:
test.h:
class test{
public:
test();
~test();
int add(Dev* dev, int a);
};
假定Dev類是一個需要和設備進行交互的類,獲取設備上的數據:
dev.h:
class Dev{
public:
Dev();
~Dev();
int getDevNum();
};
其中add方法的實現,假設就是一個加法運算,但是需要從某個設備中讀取值上來與a進行計算:(假定我們這裏的數值都必須是正數。)
test.cpp:
int test:add(Dev* dev, int a){
if(NULL == dev){
return -1;
}
if(a < 0){
return -2;
}
int sum = dev->getDevNum();//假定沒有設備,函數返回-3
if(sum < 0){
return -3;
}
sum += a;
return sum;
}
我們需要對這個add方法做單元測試,但如果測試的環境沒有設備,這個函數不就沒法完全的測試了嗎?
這個時候就需要藉助gmock,來對getDevNum打樁,讓getDevNum可以返回我們想讓它返回的任何數。
需要對getDevNum進行打樁,還需要對Dev類的實現做更改,這是因爲gmock的原理實際是依賴於C++的多態機制實現的,所以只有虛函數才能被mock,而非虛函數則無法mock。這一點也就要求我們在實現我們的類和方法時,要預先設計好,把這些依賴外部環境的實現分離開(不要與業務邏輯部分混合在一起,封裝到單獨的函數方法中),不然代碼寫一半再去使用gmock做單元測試,就往往需要對現有代碼結構做很大的更改,造成很大的時間和精力浪費。
Dev類更改如下:
class Dev{
public:
Dev();
virtual ~Dev();
virtual int getDevNum();
};
將析構函數和getDevNum函數都設爲虛函數,這樣getDevNum函數就可以被mock了。
mock一下getDevNum:
首先我們在測試代碼中定義一個MockDev類:
#include “gmock\gmock.h”
#include “dev.h”
class MockDev:public Dev{
public:
MOCK_METHOD0(getDevNum, int());
};
這樣我們就成功mock了getDevNum函數了。
MOCK_METHOD0:固定寫法,數字0表示mock的函數沒有參數,如果有2個參數,就是MOCK_METHOD2,對應的4個參數就是MOCK_METHOD4;
參數1:就是我們需要mock的函數方法名稱了;
參數2:是我們mock的函數指針類型,格式爲:返回值類型(參數1,參數2...)
如果我們的getDevNum有兩個形參:int getDevNum(int a, string b);那麼這裏的參數2格式爲:int(int a, string b)
接下來就是我們的單元測試代碼:
測試代碼include包含我們剛剛創建MockDev的頭文件,以及被測試代碼的cpp:
#include “gtest\gtest.h”
#include “gmock\gmock.h”
#include “MockDev.h”
#include “test.cpp”
using namespace testing;
TEST(TestSuiteTest, add){
int ret;
MockDev mdev;
EXPECT_CALL(mdev, getDevNum()).WillRepeatedly(Return(1));
test t;
ret = t.add(3, NULL);
EXPECT_EQ(4, -1);
ret = t.add(-1, &mdev);
EXPECT_EQ(-2, ret);
ret = t.add(3, &mdev);
EXPECT_EQ(4, ret);
}
這裏EXPECT_CALL,就是說當調用mdev的getDevNum方法的時候,返回1.
當然還有很多豐富的返回,比如第一次調用返回1,第二次調用返回2等等,其他使用方法可以參考gmock的語法。
簡單總結一下:
上面的例子雖然很簡單,但是可以看出,
1)將需要mock的函數方法定義爲虛函數;
2)我們需要在編寫代碼之初,將必要的接口分離,避免依賴外部環境的實現部分與業務邏輯部分混合。這是爲了單元測試的時候,我們可以將依賴外部環境的函數實現進行mock。
其他:
實際工程中,我們一個類中可能不僅僅是共有的函數方法,可能還有一些私有的方法,對於這些私有的函數方法,應不應該測試呢?這個目前還沒有統一的定論,需要看開發人員自己的意願,這裏給出私有方法測試的幾種方法:
1)使用#define private public粗暴地將private變成public,需要將define放在#include頭文件之前。如:
#define private public
#include “myclass.h”
2)使用friend。這個會相對友好一點,但是卻需要修改原有的代碼
3)將private方法定義爲protected,然後在測試代碼中繼承,自己定義測試的共有方法,再調用父類中的private方法:
class Num{
protected:
int add(int a, int b);
};
//測試代碼
class TestNum:public Num{
public:
int testAdd(int a, int b);
};
int TestNum:testAdd(int a, int b){
return add(a, b);
}
缺點也很明顯,還是需要更改一下原代碼。
本文爲作者原創,如需轉載,請在評論區徵得作者同意,原創鏈接:https://blog.csdn.net/anranjingsi/article/details/106084223