引言
在《Google C++單元測試架構(Gtest)系列教程之二——斷言、函數測試》中,我們瞭解了Assert 陳述式,以及如何運用TEST()進行函數測試,在TEST()的使用中,我們接觸了一個測試案例包含多個測試執行個體的組織方式。多個測試執行個體可能需要進行相識的資料配置和初始化操作,為此,Gtest提供了測試韌體(Test fixture)協助我們進行資料管理。
“落後”的方法
在瞭解測試韌體之前,我們先來看以下測試例子:
template <typename E> // E is the element type.
class Queue {
public:
Queue();
void Enqueue(const E& element);
E* Dequeue(); // Returns NULL if the queue is empty.
size_t size() const;
...
};
假設我們要對以上Queue類進行測試,根據我們之前學習到的TEST()的用法,編寫測試代碼如下:
//測試方案一
TEST(QueueTest, IsEmptyInitially) {
Queue<int> q0_;
EXPECT_EQ(0, q0_.size());
}
TEST(QueueTest, DequeueWorks) {
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
int* n = q0_.Dequeue();
EXPECT_EQ(NULL, n);
n = q1_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(1, *n);
EXPECT_EQ(0, q1_.size());
delete n;
n = q2_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(2, *n);
EXPECT_EQ(1, q2_.size());
delete n;
}
不知你是否已經發現問題所在呢?對,紅色字型的測試資料初始化部分存在重複代碼!在該例子中僅包含兩個測試執行個體,重複代碼的問題並不突出,但對於幾十個甚至上百個測試執行個體而言,我們就需要另一種方式管理我們的初始化資料了。
測試韌體(Test fixture)
測試韌體的作用在於管理兩個或多個測試執行個體都會使用到的資料,使用測試韌體完成上述測試,方法如下:
首先我們需要定義一個韌體類(fixture class),一般韌體類以FooTest的形式命名,其中Foo為被測類的名稱:
class QueueTest : public ::testing::Test {
protected:
virtual void SetUp() {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// virtual void TearDown() {}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
定義韌體類的方法為:
- 寫一個繼承自::test::Test的類,為使該類的子類能訪問到該類的資料,使用public或protected作為存取控制標識;
- 在該類中,定義測試執行個體將用到的資料;
- 使用SetUp()方法或預設建構函式作資料初始化操作,使用TearDown()方法或解構函式作資料清理操作,注意SetUp()和TearDown()的拼字;
- 如有需要,還可以在該類中定義成員函數,正如初始化資料,這裡所定義的成員函數也可被測試執行個體重複使用。
接下來我們來看如何編寫相應的測試執行個體,首先我們要用到一個新的宏:
TEST_F(test_case_name, test_name) {
... test body ...
}
TEST_F()必須在測試韌體定義之後才能使用,其兩個參數含義與TEST()的參數含義相同,但TEST_F()的第一個參數必須為韌體類的名稱。
結合上述QueueTest測試韌體,我們編寫測試代碼如下:
//測試方案二
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(0, q0_.size());
}
TEST_F(QueueTest, DequeueWorks) {
int* n = q0_.Dequeue();
EXPECT_EQ(NULL, n);
n = q1_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(1, *n);
EXPECT_EQ(0, q1_.size());
delete n;
n = q2_.Dequeue();
ASSERT_TRUE(n != NULL);
EXPECT_EQ(2, *n);
EXPECT_EQ(1, q2_.size());
delete n;
}
可以看出TEST_F()的使用方法與TEST()差別不大,當以上兩個測試執行個體運行時,Gtest為我們做了以下事情:
- 構造一個QueueTest對象(假設為t1);
- 調用t1.SetUp()初始化t1對象;
- 第一個測試執行個體(IsEmptyInitially)使用t1進行測試;
- 調用t1.TearDown()進行資料清理;
- 銷毀對象t1;
- 建立一個新的QueueTest對象,對下一個測試執行個體DequeueWorks重複以上步驟。
可見Gtest通過建立和銷毀韌體類對象,為每一個測試執行個體建立了一份獨立的初始化資料,上面的兩個測試方案的目的和結果完全一樣,但方案二通過使用測試韌體,杜絕了資料初始化帶來的重複代碼。
韌體類(Fixture class)
C++類具有可繼承的特點,這樣我們可以靈活地定義韌體類,我們可以把多個韌體類共有的特性抽象出來形成一個基類,以進一步達到代碼複用、資料複用的效果,來看下面一個例子。
class QuickTest : public testing::Test {
protected:
// This is a good place to record the start time.
virtual void SetUp() {
start_time_ = time(NULL);
}
// check if the test was too slow.
virtual void TearDown() {
// Gets the time when the test finishes
const time_t end_time = time(NULL);
// Asserts that the test took no more than ~5 seconds.
EXPECT_TRUE(end_time - start_time_ <= 5) << "The test took too long.";
}
該韌體類對測試執行個體的已耗用時間作一個簡單的分析,其利用了SetUp()在測試執行個體運行前執行、TearDown()在測試執行個體運行後執行的特點,已耗用時間超過5秒的測試執行個體將檢測失敗,注意SetUp()和TearDown()函數中也可以使用Assert 陳述式。
假設我們對Queue類的測試執行個體有執行時間限制,我們可以編寫繼承自QuickTest的韌體類:
class QueueTest : public QuickTest {
//......
};
經過這樣定義,與QueueTest相關聯的測試執行個體運行時,其執行時間將得到檢測。
小結
本文介紹了使用Gtest測試韌體(Test fixture)的原因及方法,最後提出可以通過類繼承的方式靈活定義測試韌體。下一節將介紹Gtest值參數化、型別參數化的使用方法。
Reference: googletest project