哈喽,大家好,我是春哥、欢迎来到本期分享。
1.问题
在测试金字塔理论中,单元测试被视为软件开发质量保证的基石,它对于构建高质量、稳定的系统至关重要。单元测试能显著提升代码质量,它有助于在软件开发过程中尽早发现问题,降低开发成本并提高整体效率。特别是在代码重构和优化的场景下,完备的单元测试会让你对重构过程更具信心。
然而,许多单元测试框架往往是庞大且全面的重量级框架,使用起来相对复杂。实际工作场景中,我们并不需要如此多的功能。因此,构建一个「轻量级」
且「易于上手」
的单元测试框架变得至关重要和迫切。
春哥针对这一需求,使用C++实现了轻量级的单元测试框架UnitTest,旨在抛砖引玉,希望能为大家带来实际帮助。
2.架构设计
这个UnitTest单元测试框架是如何设计的呢?让我们先来看一下框架中各个类之间的关系图。
2.1 TestCase
TestCase为测试用例基类,可以在它的基础上派生出具体的测试用例类。
首先,它定义了一个纯虚函数Run(),用于执行测试用例的具体逻辑。这个函数需要在具体的测试用例类中实现。
其次,它定义了一个虚函数TestCaseRun(),它调用了Run()函数,并将执行结果保存在result_成员变量中。必要时可以在具体的测试用例类中重写,以实现特定的测试逻辑。
接着,它定义了一个Result()函数,用于获取测试结果。这个函数返回一个bool类型的值,表示测试是否通过。
然后,它定义了一个SetResult()函数,用于设置测试结果。这个函数接受一个bool类型的参数,表示测试是否通过。
最后,它定义了一个CaseName()函数,用于获取测试用例的名称。这个函数返回一个std::string类型的值,表示测试用例的名称。
在这个类的构造函数中,它接受一个std::string类型的参数case_name,用于设置测试用例的名称。这个参数会被保存在case_name_成员变量中。
2.2 TestCaseA、TestCaseB
TestCaseA和TestCaseB是具体的测试用例类,它们都继承自TestCase类,是单元测试用例的具体实现,也是框架中最小的单元测试单位。
2.3 UnitTestCore
UnitTestCore为单元测试框架的核心类,它提供了注册测试用例、运行测试用例的功能,是单元测试框架的「引擎」
。
首先,它定义了一个静态函数GetInstance(),用于获取单例对象。这个函数使用了静态局部变量,保证了线程安全。
接着,它定义了一个Run()函数,用于运行所有注册的测试用例。这个函数接受两个参数,分别是命令行参数的数量和参数数组。在函数内部,它会遍历所有注册的测试用例,并支持通过正则表达式匹配测试用例名称来过滤测试用例,然后依次执行它们的TestCaseRun()函数。在执行完每个测试用例后,它会根据测试结果更新success_count_和failure_count_成员变量,并输出测试结果。如果有测试用例执行失败,它会将result_成员变量设置为false。
然后,它定义了一个Register()函数,用于注册测试用例。这个函数接受一个TestCase类型的指针参数,表示要注册的测试用例。在函数内部,它会将测试用例指针保存在test_cases_成员变量中,并返回测试用例指针。
最后,它定义了一些私有成员变量,包括result_、success_count_、failure_count_和test_cases_。这些成员变量用于保存测试结果和测试用例集合。
UnitTestCore类提供了注册测试用例、运行测试用例的基本功能,可以帮助我们更加方便的管理和执行单元测试用例。
3.源码
talk is cheap, show me the code
看完类的关系图后,让我们来看一下具体的代码实现和如何使用该框架的demo示例。
3.1 核心代码
我们单元测试框架的核心代码文件unittestcore.hpp,内容如下所示。
#pragma once
#include <iostream>
#include <regex>
#include <string>
#include <vector>
namespace UnitTest {
static const char *kGreenBegin = "\033[32m";
static const char *kRedBegin = "\033[31m";
static const char *kColorEnd = "\033[0m";
class TestCase {
public:
virtual void Run() = 0;
virtual void TestCaseRun() { Run(); }
bool Result() { return result_; }
void SetResult(bool result) { result_ = result; }
std::string CaseName() { return case_name_; }
TestCase(std::string case_name) : case_name_(case_name) {}
private:
bool result_{true};
std::string case_name_;
};
class UnitTestCore {
public:
static UnitTestCore *GetInstance() {
static UnitTestCore instance;
return &instance;
}
int Run(int argc, char *argv[]) {
result_ = true;
failure_count_ = 0;
success_count_ = 0;
std::cout << kGreenBegin << "[==============================] Running " << test_cases_.size() << " test case."
<< kColorEnd << std::endl;
constexpr int kFilterArgc = 2;
for (size_t i = 0; i < test_cases_.size(); i++) {
if (argc == kFilterArgc) {
// 第二参数时,做用例CaseName来做过滤
if (not std::regex_search(test_cases_[i]->CaseName(), std::regex(argv[1]))) {
continue;
}
}
std::cout << kGreenBegin << "Run TestCase:" << test_cases_[i]->CaseName() << kColorEnd << std::endl;
test_cases_[i]->TestCaseRun();
std::cout << kGreenBegin << "End TestCase:" << test_cases_[i]->CaseName() << kColorEnd << std::endl;
if (test_cases_[i]->Result()) {
success_count_++;
} else {
failure_count_++;
result_ = false;
}
}
std::cout << kGreenBegin << "[==============================] Total TestCase:" << test_cases_.size() << kColorEnd
<< std::endl;
std::cout << kGreenBegin << "Passed:" << success_count_ << kColorEnd << std::endl;
if (failure_count_ > 0) {
std::cout << kRedBegin << "Failed:" << failure_count_ << kColorEnd << std::endl;
}
return 0;
}
TestCase *Register(TestCase *test_case) {
test_cases_.push_back(test_case);
return test_case;
}
private:
bool result_{true};
int32_t success_count_{0};
int32_t failure_count_{0};
std::vector<TestCase *> test_cases_; // 测试用例集合
};
#define TEST_CASE_CLASS(test_case_name) \
class test_case_name : public UnitTest::TestCase { \
public: \
test_case_name(std::string case_name) : UnitTest::TestCase(case_name) {} \
virtual void Run(); \
\
private: \
static UnitTest::TestCase *const test_case_; \
}; \
UnitTest::TestCase *const test_case_name::test_case_ = \
UnitTest::UnitTestCore::GetInstance()->Register(new test_case_name(#test_case_name)); \
void test_case_name::Run()
#define TEST_CASE(test_case_name) TEST_CASE_CLASS(test_case_name)
#define ASSERT_EQ(left, right) \
if ((left) != (right)) { \
std::cout << UnitTest::kRedBegin << "assert_eq failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
<< "!=" << (right) << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_NE(left, right) \
if ((left) == (right)) { \
std::cout << UnitTest::kRedBegin << "assert_ne failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
<< "==" << (right) << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_LT(left, right) \
if ((left) >= (right)) { \
std::cout << UnitTest::kRedBegin << "assert_lt failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
<< ">=" << (right) << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_LE(left, right) \
if ((left) > (right)) { \
std::cout << UnitTest::kRedBegin << "assert_le failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) << ">" \
<< (right) << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_GT(left, right) \
if ((left) <= (right)) { \
std::cout << UnitTest::kRedBegin << "assert_gt failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) \
<< "<=" << (right) << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_GE(left, right) \
if ((left) < (right)) { \
std::cout << UnitTest::kRedBegin << "assert_ge failed at " << __FILE__ << ":" << __LINE__ << ". " << (left) << "<" \
<< (right) << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_TRUE(expr) \
if (not(expr)) { \
std::cout << UnitTest::kRedBegin << "assert_true failed at " << __FILE__ << ":" << __LINE__ << ". " << (expr) \
<< " is false" << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define ASSERT_FALSE(expr) \
if ((expr)) { \
std::cout << UnitTest::kRedBegin << "assert_false failed at " << __FILE__ << ":" << __LINE__ << ". " << (expr) \
<< " if true" << UnitTest::kColorEnd << std::endl; \
SetResult(false); \
return; \
}
#define RUN_ALL_TESTS() \
int main(int argc, char *argv[]) { return UnitTest::UnitTestCore::GetInstance()->Run(argc, argv); }
} // namespace UnitTest
通过上面的代码可以看出,我们的单元测试框架代码非常简单。除了定义了TestCase和UnitTestCore这两个类之外,还有一些辅助宏的定义。
3.2 辅助宏
我们的单元测试框架提供了一系列的辅助宏,可以帮助开发者快速搭建和执行测试用例。下面,我们将详细介绍这些宏的作用和用法。通过使用这些宏,开发者可以更加方便地编写测试用例和断言语句,从而提高测试代码的可读性和可维护性。
3.2.1 TEST_CASE_CLASS
这个宏用于定义测试用例类。它接受一个参数test_case_name,表示测试用例类的名称。这个宏它定义了一个继承自UnitTest::TestCase的测试用例类,并实现了Run()函数。同时,它还定义了一个静态成员变量test_case_,用于注册测试用例。在宏定义的最后,它使用UnitTest::UnitTestCore::GetInstance()->Register()函数将测试用例注册到测试框架中。
3.2.2 TEST_CASE
这个宏用于定义测试用例。这个宏接受一个参数test_case_name,表示测试用例的名称。在宏定义中,它使用TEST_CASE_CLASS宏定义测试用例类,并将测试用例类的名称作为参数传递给TEST_CASE_CLASS宏。
3.2.3 ASSERT_XXX
ASSERT_XXX是一系列的宏,用于在每个单独的测试用例中校验执行结果是否符合预期。如果执行结果不符合预期,宏会中断当前用例的执行,并标记测试用例执行失败。
3.2.4 RUN_ALL_TESTS
这个宏用于运行所有注册的测试用例。这个宏定义了一个main()函数,并调用UnitTest::UnitTestCore::GetInstance()->Run()函数来运行所有的测试用例。
3.3 demo示例
接下来,我们将展示单元测试框架的demo示例。在单元测试文件中,只需使用include预编译指令来引入unittestcore.hpp文件,然后使用辅助宏。如此简单,轻松上手,是不是so easy呀!!!
#include <iostream>
#include "unittestcore.hpp"
using namespace std;
int func(int value) { return value; }
TEST_CASE(assert_eq) {
ASSERT_EQ(func(1), 1);
ASSERT_EQ(1, 1);
ASSERT_EQ(1 + 2, 1 + 1 + 1);
}
TEST_CASE(assert_true) {
ASSERT_TRUE(true);
ASSERT_TRUE(10 > 2);
ASSERT_TRUE(func(1) == 1);
}
TEST_CASE(assert_false) {
ASSERT_FALSE(false);
ASSERT_FALSE(2 > 10);
ASSERT_FALSE(func(1) != 1);
}
TEST_CASE(assert_ne) {
ASSERT_NE(10, 11);
ASSERT_NE(10 + 10, 11 + 11);
ASSERT_NE(func(1), func(1) + func(9));
}
TEST_CASE(assert_gt) {
ASSERT_GT(1, 0);
ASSERT_GT(func(1), 0);
ASSERT_GT(func(3), func(1) + func(1));
}
TEST_CASE(assert_ge) {
ASSERT_GE(1, 1);
ASSERT_GE(func(1), 1);
ASSERT_GE(func(3), func(1) + func(1) + func(1));
}
TEST_CASE(assert_lt) {
ASSERT_LT(0, 1);
ASSERT_LT(func(1), 2);
ASSERT_LT(func(3), func(1) + func(1) + func(1) + func(1));
}
TEST_CASE(assert_le) {
ASSERT_LE(0, 1);
ASSERT_LE(1, 1);
ASSERT_LE(func(2), 2);
ASSERT_LE(func(3), func(1) + func(1) + func(1));
}
RUN_ALL_TESTS();
在 CentOS 云服务器上,编译和运行 demo 示例的效果如下所示。
[root@VM-114-245-centos UnitTest]# make
g++ -g -O2 -Wall -Werror -pipe -m64 -std=c++11 -c demo_test.cpp -o demo_test.o
g++ -g -O2 -Wall -Werror -pipe -m64 -std=c++11 ./demo_test.o -o UnitTest
Type UnitTest to execute the program.
[root@VM-114-245-centos UnitTest]# ./UnitTest
[==============================] Running 8 test case.
Run TestCase:assert_eq
End TestCase:assert_eq
Run TestCase:assert_true
End TestCase:assert_true
Run TestCase:assert_false
End TestCase:assert_false
Run TestCase:assert_ne
End TestCase:assert_ne
Run TestCase:assert_gt
End TestCase:assert_gt
Run TestCase:assert_ge
End TestCase:assert_ge
Run TestCase:assert_lt
End TestCase:assert_lt
Run TestCase:assert_le
End TestCase:assert_le
[==============================] Total TestCase:8
Passed:8
[root@VM-114-245-centos UnitTest]#
3.4 完整项目
春哥已将此框架开源并托管在GitHub上,项目页面包含了详细的使用说明和编译脚本。传送门:github.com/wanmuc/Unit… 。欢迎大家积极参与,进行fork、star以及提出issue。如果因为网络问题导致无法下载,请随时私信我,我会直接将项目完整源码发送给您。
4.落地实施
我们都知道人们天生有懒惰的一面,春哥也不例外,也不太想编写单元测试代码。那么如何在团队中推广并实施单元测试呢?
首先,需要选择一个合适的单元测试框架,降低个人或团队的使用成本,减少「抵触心理」
。春哥已经在前文提供了一个轻量级的实现,供大家参考。
其次,团队成员需要达成共识,认识到单元测试带来的好处,并在项目排期中给予相应的「工时支持」
。这样一来,大家的抵触心理会进一步降低。
最后,我们要强调的是,编写单元测试代码和业务代码同样「重要且严肃」
,需要认真对待。同时,保持单元测试代码的可读性和可维护性也是至关重要的。
5.总结
单元测试、接口测试、集成测试和端到端测试共同构建了测试金字塔,其中单元测试是其基石。在未来的分享中,春哥将持续运用这个单元测试框架,以提高春哥开源项目的代码质量。
今天的内容就到这里,如果你喜欢我的分享,记得点赞、评论、转发加关注。我会持续为大家带来更多有价值的内容,你的支持是我最大的动力。感谢你的关注和支持!