【C++ UnitTest】基于Google Test 编写 test fixture
什么是test fixture
测试固件(test fixture)是一种用于测试的环境设置,它用于确保在测试运行期间有一致的测试环境。测试固件允许你在多个测试用例之间共享初始化和清理代码,并且可以确保每个测试用例在相同的环境下执行,从而提高了测试的可靠性和一致性。
怎么使用test fixture
当业务代码需要你实现一个新的Class,不管是什么类,必然有相应的函数和初始化参数,对于类中定义的函数实现基本基于同一套初始化参数或者不是基于某些参数,但始终都是在同一个类中的实现。对于这个类的测试需求,就可以使用test fixture。
以官方文档中的代码实例,代码中实现了队列的创建、添加、删除、节点访问等功能。
#include <stddef.h>
// Queue is a simple queue implemented as a singled-linked list.
//
// The element type must support copy constructor.
template <typename E> // E is the element type
class Queue;
// QueueNode is a node in a Queue, which consists of an element of
// type E and a pointer to the next node.
template <typename E> // E is the element type
class QueueNode {
friend class Queue<E>;
public:
// Gets the element in this node.
const E& element() const { return element_; }
// Gets the next node in the queue.
QueueNode* next() { return next_; }
const QueueNode* next() const { return next_; }
private:
// Creates a node with a given element value. The next pointer is
// set to NULL.
explicit QueueNode(const E& an_element)
: element_(an_element), next_(nullptr) {}
// We disable the default assignment operator and copy c'tor.
const QueueNode& operator=(const QueueNode&);
QueueNode(const QueueNode&);
E element_;
QueueNode* next_;
};
template <typename E> // E is the element type.
class Queue {
public:
// Creates an empty queue.
Queue() : head_(nullptr), last_(nullptr), size_(0) {}
// D'tor. Clears the queue.
~Queue() { Clear(); }
// Clears the queue.
void Clear() {
if (size_ > 0) {
// 1. Deletes every node.
QueueNode<E>* node = head_;
QueueNode<E>* next = node->next();
for (;;) {
delete node;
node = next;
if (node == nullptr) break;
next = node->next();
}
// 2. Resets the member variables.
head_ = last_ = nullptr;
size_ = 0;
}
}
// Gets the number of elements.
size_t Size() const { return size_; }
// Gets the first element of the queue, or NULL if the queue is empty.
QueueNode<E>* Head() { return head_; }
const QueueNode<E>* Head() const { return head_; }
// Gets the last element of the queue, or NULL if the queue is empty.
QueueNode<E>* Last() { return last_; }
const QueueNode<E>* Last() const { return last_; }
// Adds an element to the end of the queue. A copy of the element is
// created using the copy constructor, and then stored in the queue.
// Changes made to the element in the queue doesn't affect the source
// object, and vice versa.
void Enqueue(const E& element) {
QueueNode<E>* new_node = new QueueNode<E>(element);
if (size_ == 0) {
head_ = last_ = new_node;
size_ = 1;
}
else {
last_->next_ = new_node;
last_ = new_node;
size_++;
}
}
// Removes the head of the queue and returns it. Returns NULL if
// the queue is empty.
E* Dequeue() {
if (size_ == 0) {
return nullptr;
}
const QueueNode<E>* const old_head = head_;
head_ = head_->next_;
size_--;
if (size_ == 0) {
last_ = nullptr;
}
E* element = new E(old_head->element());
delete old_head;
return element;
}
// Applies a function/functor on each element of the queue, and
// returns the result in a new queue. The original queue is not
// affected.
template <typename F>
Queue* Map(F function) const {
Queue* new_queue = new Queue();
for (const QueueNode<E>* node = head_; node != nullptr;
node = node->next_) {
new_queue->Enqueue(function(node->element()));
}
return new_queue;
}
private:
QueueNode<E>* head_; // The first node of the queue.
QueueNode<E>* last_; // The last node of the queue.
size_t size_; // The number of elements in the queue.
// We disallow copying a queue.
Queue(const Queue&);
const Queue& operator=(const Queue&);
};
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
由于使用的是test fixture,这里使用的是TEST_F
而不是TEST
。
测试的需求包括对QueueNode类默认初始化参数的测试项 DefaultConstructor
,QueueNode类中Dequeue成员函数的功能测试项Dequeue
,QueueNode类中Map成员函数的功能测试项Map
。当然你也可以添加对Enqueue
函数的测试项,只要可以符合你的预期,无论是预期对还是预期错。
包含测试夹具的UT代码:
#include "gtest/gtest.h"
namespace {
// To use a test fixture, derive a class from testing::Test.
class QueueTestSmpl3 : public testing::Test {
protected: // You should make the members protected s.t. they can be
// accessed from sub-classes.
// virtual void SetUp() will be called before each test is run. You
// should define it if you need to initialize the variables.
// Otherwise, this can be skipped.
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// virtual void TearDown() will be called after each test is run.
// You should define it if there is cleanup work to do. Otherwise,
// you don't have to provide it.
//
// virtual void TearDown() {
// }
// A helper function that some test uses.
static int Double(int n) { return 2 * n; }
// A helper function for testing Queue::Map().
void MapTester(const Queue<int>* q) {
// Creates a new queue, where each element is twice as big as the
// corresponding one in q.
const Queue<int>* const new_q = q->Map(Double);
// Verifies that the new queue has the same size as q.
ASSERT_EQ(q->Size(), new_q->Size());
// Verifies the relationship between the elements of the two queues.
for (const QueueNode<int>* n1 = q->Head(), *n2 = new_q->Head();
n1 != nullptr; n1 = n1->next(), n2 = n2->next()) {
EXPECT_EQ(2 * n1->element(), n2->element());
}
delete new_q;
}
// Declares the variables your tests want to use.
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
// When you have a test fixture, you define a test using TEST_F
// instead of TEST.
// Tests the default c'tor.
TEST_F(QueueTestSmpl3, DefaultConstructor) {
// You can access data in the test fixture here.
EXPECT_EQ(0u, q0_.Size());//默认的q0_并没有赋值
}
// Tests Dequeue().
TEST_F(QueueTestSmpl3, Dequeue) {
int* n = q0_.Dequeue();
EXPECT_TRUE(n == nullptr);//当q0_的size=0时,返回空指针
n = q1_.Dequeue();//删除头节点并返回头节点信息
ASSERT_TRUE(n != nullptr);//断言返回值不为空,如果出错则不往后执行
EXPECT_EQ(1, *n);
EXPECT_EQ(0u, q1_.Size());
delete n;
n = q2_.Dequeue();
ASSERT_TRUE(n != nullptr);
EXPECT_EQ(2, *n);
EXPECT_EQ(1u, q2_.Size());
delete n;
}
// Tests the Queue::Map() function.
TEST_F(QueueTestSmpl3, Map) {
MapTester(&q0_);
MapTester(&q1_);
MapTester(&q2_);
}
} // namespace
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
测试执行结果
这里一共测试了三个分项,都符合预期结果。
如果我这里修改其中一条测试的预期,故意让测试不通过,会怎样?
// Tests Dequeue().
TEST_F(QueueTestSmpl3, Dequeue) {
int* n = q0_.Dequeue();
EXPECT_TRUE(n == nullptr);
n = q1_.Dequeue();//这里执行完Dequeue,返回的参数不应该为nullptr
ASSERT_TRUE(n == nullptr);//ASSERT_TRUE(n != nullptr);
1234567
执行结果中可以看到原本全都绿色代表PASS的结果,出现了红色的FAILED
信息,并且是在[ RUN ] QueueTestSmpl3.Dequeue
后出现的FAILED
。由于我是提前清楚自己修改的UT是在哪一行,从执行结果上也基本能锁定出错的位置和信息。
est.cpp(66): error: Value of: n == nullptr
Actual: false
Expected: true
123
不同的预期(EXCEPT_)有不同的结果,无论你是判断“对”还是判断“错”,取决编写UT的人怎么去定义它。
SetUp()和TearDown()
SetUp()
和 TearDown()
是用于测试固件(test fixture)的特殊方法,它们分别用于在每个测试用例运行前和运行后执行一些初始化和清理工作。
class QueueTestSmpl3 : public testing::Test {
protected: // You should make the members protected s.t. they can be
// accessed from sub-classes.
// virtual void SetUp() will be called before each test is run. You
// should define it if you need to initialize the variables.
// Otherwise, this can be skipped.
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
1234567891011
SetUp():
SetUp() 方法在每个测试用例运行之前被调用。它通常用于设置测试所需的环境、初始化对象、加载数据等操作。通过在 SetUp() 中进行这些初始化操作,你可以确保每个测试用例在相同的测试环境下执行,提高了测试的一致性和可靠性。
这里的SetUp() 里分别对q1_,q2_变量进行了赋值,以便后续的测试工作。
TearDown():
TearDown() 方法在每个测试用例运行之后被调用。它通常用于清理测试过程中所产生的临时资源、释放内存、关闭文件或者数据库连接等操作。通过在 TearDown() 中进行这些清理工作,你可以确保在测试结束后恢复到一个干净的状态,避免了测试中可能产生的副作用。
这里的TearDown()并没有对原TearDown()函数进行override。
SetUpTestCase()和TearDownTestCase()
此外,还有本段代码中没有涉及的特殊方法,SetUpTestCase() 和 TearDownTestCase() 是用于在unittest中设置测试固件的特殊方法,它们用于在测试用例集合(test case class)的开始和结束时执行一些初始化和清理工作。
SetUpTestCase():
SetUpTestCase() 方法在测试用例集合(test case class)中的第一个测试用例运行之前被调用,通常用于执行一些针对整个测试用例集合的初始化操作。比如,可能需要在这里建立测试用例之间共享的环境、加载测试数据等操作。
TearDownTestCase():
TearDownTestCase() 方法在测试用例集合中的最后一个测试用例运行之后被调用,通常用于执行一些针对整个测试用例集合的清理工作。比如,可能需要在这里清理测试过程中产生的临时文件、释放资源等操作。
这两个方法的使用场景与 SetUp()
和 TearDown()
类似,不同之处在于它们是针对整个测试用例集合的,而不是针对单个测试用例。通过在测试用例集合的开始和结束时执行一些初始化和清理工作,你可以确保测试环境的一致性,并在测试结束后保持环境的干净和稳定。