前言:
当人们谈到测试框架的时候,首先想到的就是 google 的 gtest, 想着怎么在代码中集成 gtest 的框架,来实现自身代码的测试。 然后就巴拉巴拉的费了老大劲将 gtest 嵌入到自己的代码中来。
诚然,在自身程序接口稳定,且代码量达到一定程度的时候,上 gtest 是一个不错的选择。
但是当自身代码不是很多,并不想带上一个厚重的第三方库的时候,那就得自己想办法搭建测试框架了。
本文中我们来探讨下,在不依赖第三方库的情况下,如何搭建自己的测试框架。
文章目录
- 前言:
- 测试框架的组成
- 一个测试框架的示例
- 本测试框架的主要组成
- 各基本组成的主要业务逻辑
- 测试用例的自动注册
- 测试用例的执行
- 测试结果的接收和呈现
- 总结
测试框架的组成
当我们来设计测试框架的时候,总会想测试框架最基本的组成应该是什么。
我总结三点:
- 测试用例的自动注册
- 测试用例的执行(自动/半自动/手动)
- 测试结果的接收和呈现。
即:测试框架被带起时,要能将待测试项进行自动注册;而当要执行测试用例的时候,又能够手动或自动的执行测试用例;测试用例执行完之后,能将结果汇总并呈现给开发者或用户。
那么我们就通过一个例子,来看下一种高效的解决方案设计是怎么执行这些逻辑的。
一个测试框架的示例
本测试框架的主要组成
class or struct | description |
---|---|
DemoTestStatus | 一个枚举,定义了测试状态(成功、非致命失败、致命失败) |
DemoTestResult | 一个结构体,用于存储测试结果和相关信息。 |
DemoTest | 一个抽象基类,定义了测试用例的基本接口和执行流程。 |
DemoTestInfo | 一个类,用于存储测试用例的元数据和结果。 |
DemoTestFactoryBase | 一个抽象工厂类,用于创建测试实例。 |
DemoTestManager | 一个类,提供测试用例的注册、执行、结果收集和展示等功能。 |
一系列测试宏 | 用于简化测试用例的创建和注册。 |
其简化后的结构设计如下:
enum DemoTestStatus
{kSuccess,kNotFatalFailure,kFatalFailure
};struct DemoTestResult
{DemoTestStatus resStatus;std::string resInfo;DemoTestResult() : resStatus(kSuccess), resInfo("Success") {}
};class DemoTest
{
public:virtual ~DemoTest() {}static bool hasFatalFailure();protected:friend class DemoTestinfo;DemoTest();// run the testvoid run();// run the test after the test has been set up.virtual void testing() = 0;// resultDemoTestResult* result() const;void setTestResult(const DemoTestStatus& status, const char* pFmt, ...);
};class DemoTestInfo
{
public:DemoTestInfo(const std::string& name, const std::string& desc, DemoTestFactoryBase* pFactory);~DemoTestInfo();const std::string name() const;const std::string desc() const;void run();DemoTestResult* result() const;private:std::string m_name;std::string m_desc;DemoTestFactoryBase* m_pFactory = nullptr;DemoTestResult m_pResult;
};class DemoTestFactoryBase
{
public:virtual ~DemoTestFactoryBase() {}virtual DemoTest* createTest() = 0;protected:DemoTestFactoryBase() {}DemoTestFactoryBase(const DemoTestFactoryBase&) = delete;DemoTestFactoryBase& operator=(const DemoTestFactoryBase&) = delete;
};template <typename Ts>
class DemoTestFactory : public DemoTestFactoryBase
{
public:virtual DemoTest* createTest() override{return new Ts();}
};extern DemoTestInfo* newAndRegisterTestInfo(const std::string& name, const std::string& desc, DemoTestFactoryBase* pFactory);// test macro#define DEMO_API_TEST_CASE_NAME(test_case_name) #test_case_name
#define DEMO_API_TEST_CLASS_NAME(test_case_name) Demo##Test##DEMO_API_TEST_CASE_NAME#define DEMO_API_TEST_FACTORY(test_class_name) (new DemoTestFactory<test_class_name>())#define DEMO_API_TEST(test_leaf_class, test_base_class) \class DEMO_API_TEST_CLASS_NAME(test_leaf_class) : public test_base_class \{ \public: \DEMO_API_TEST_CLASS_NAME(test_leaf_class)() {} \void testing() override; \private: \static DemoTestInfo* s_pTestInfo; \DEMO_API_TEST_CLASS_NAME(test_leaf_class)(const DEMO_API_TEST_CLASS_NAME(test_leaf_class)&) = delete; \DEMO_API_TEST_CLASS_NAME(test_leaf_class)& operator=(const DEMO_API_TEST_CLASS_NAME(test_leaf_class)&) = delete; \}; \\DemoTestInfo* DEMO_API_TEST_CLASS_NAME(test_leaf_class)::s_pTestInfo = newAndRegisterTestInfo(DEMO_API_TEST_CASE_NAME(test_leaf_class), std::string(), DEMO_API_TEST_FACTORY(DEMO_API_TEST_CLASS_NAME(test_leaf_class))); \\void DEMO_API_TEST_CLASS_NAME(test_leaf_class)::testing()#define DEMO_API_DEFAULT_TEST(test_case_name) DEMO_API_TEST(test_case_name, DemoTest)#define DEMO_API_TEST_RESULT(result) setTestResult(result, "The error occurred in the line:%d, file: %s.\n", __LINE__, __FILE__)class DemoTestManager
{
public:static DemoTestManager* instance();void runTest(const std::string name);void runAllTests();DemoTestInfo* currentTestInfo() const;void setCurrentTestInfo(DemoTestInfo* pInfo);bool hasSameTest() const;void registerTestInfo(DemoTestInfo* pTestInfo);void unregisterTestInfo(const std::string& name);private:DemoTestManager();~DemoTestManager();void showResult(DemoTestInfo* pTestInfo);void showResults(const std::vector<DemoTestInfo*>& testInfos);// SystemStatus checkDefaultLibExistOnce();DemoTestInfo* m_pCurrentTestInfo;std::vector<DemoTestInfo*> m_testInfos;std::vector<DemoTestInfo*> m_smaeNameTestinfos;struct Record{std::vector<DemoTestResult> results;};std::map<DemoTestInfo*, Record> m_records;UiElement* m_pElement = nullptr;
};
当用户写测试用例时,可以这样写:
DEMO_API_DEFAULT_TEST(test_exp_1)
{if (...) {DEMO_API_TEST_RESULT(kNotFatalFailure);return;}...return;
}
各基本组成的主要业务逻辑
测试用例的自动注册
本架构中测试用例的注册是在该模块被加载时,静态被注册到测试框架中的,并以按钮的形式被添加在主程序界面窗口上的。
我们来具体看下业务逻辑:
- 将
DEMO_API_DEFAULT_TEST(test_exp_1)
展开,我们看下:
class DemoTesttest_exp_1 : public DemoTest
{
public: DemoTesttest_exp_1() {} void testing() override; private: static DemoTestInfo* s_pTestInfo;DemoTesttest_exp_1(const DemoTesttest_exp_1&) = delete; DemoTesttest_exp_1& operator=(const DemoTesttest_exp_1&) = delete;
}; DemoTestInfo* DemoTesttest_exp_1::s_pTestInfo = newAndRegisterTestInfo("test_exp_1", std::string(), (new DemoTestFactory<DemoTesttest_exp_1>())); void DemoTesttest_exp_1::testing()
{if (...) {DEMO_API_TEST_RESULT(kNotFatalFailure);return;}...return;
}
- 这其中我们看到
DemoTesttest_exp_1
这个子测试用例类有一个静态成员变量:s_pTestInfo
,它是在该模块启动阶段就被初始化了。 - 我们在来看下它的初始化逻辑。即展开看下
newAndRegisterTestInfo
函数的逻辑。
DemoTestInfo* newAndRegisterTestInfo(const std::string& name, const std::string& desc, DemoTestFactoryBase* pFactory)
{auto pTestInfo = new DemoTestInfo(name, desc, pFactory);DemoTestManager::instance()->registerTestInfo(pTestInfo);return pTestInfo;
}
- 在传入参数给这个函数的时候,
new
了一个子测试用例类的工厂类,传入函数之后,new
出了与之匹配的自测试用例信息类,然后将其注册给DemoTestManager
. - 我们来继续看下
registerTestInfo()
里面的执行逻辑:
void DemoTestManager::registerTestInfo(DemoTestInfo* pTestInfo)
{if (!pTestInfo) {return;}...UiMenu* pMenu = m_pElement->menu();m_testInfos.push_back(pTestInfo);UiElement* pTestCaseElem = pMenu->addElement(pTestInfo->name());pTestCaseElem->sigTriggered().connect([this](UiElement* pElem) { runTest(pElem->text()); });
}
// 注: 其中以 UiXXX 写法的类均是对 QT API类的二次封装,读者再看时,可以将其替换成QT对应的逻辑即可。
- 上面那段业务逻辑,其实核心的就是给该自测试用例关联创建一个可视化的子按钮,点击按钮即可触发该测试用例的逻辑(槽函数机制)。
测试用例的执行
- 我们来具体看下,当我们触发了槽函数,又是如何一步一步的执行子测试用例的。
- 先看下
runTest()
的具体业务逻辑:
void DemoTestManager::runTest(const std::string name)
{for (auto pTestInfo : m_testInfos) {if (pTestInfo->name() == name) {pTestInfo->run();m_records[pTestInfo].results.push_back(*pTestInfo->result());showResult(pTestInfo);break;}}
}
- 其中核心的一条就是调用
DemoTestInfo
的run()
接口:
void DemoTestInfo::run()
{DemoTest* pTest = m_pFactory->createTest();if (!DemoTest::hasFatalFailure()) {pTest->run();}delete pTest;pTest = nullptr;
}
- 这里我们可以看到,最终执行的是子测试用例的
run()
接口,即其内部实际上又是调用的子测试用例的testing()
接口,就是本节开头的:
void DemoTesttest_exp_1::testing()
{if (...) {DEMO_API_TEST_RESULT(kNotFatalFailure);return;}...return;
}
- 至此,完成该测试项的执行。
测试结果的接收和呈现
- 好,完成了测试用例的执行之后,测试用例执行成功与否,失败处的相关信息记录又是怎样的呢?我们继续来看下:
- 接着展开子测试用例的测试接口
testing()
, 我们会看到承接逻辑判断时,会用一个宏去记录判断的结果,即:DEMO_API_TEST_RESULT
#define DEMO_API_TEST_RESULT(result) \
setTestResult(result, "The error occurred in the line:%d, file: %s.\n", __LINE__, __FILE__)
- 将这个函数展开:
void DemoTest::setTestResult(const DemoTestStatus& status, const char* pFmt, ...)
{result()->resStatus = status;std::string info;if (status == kSuccess) {info.append("success");} else {va_list args;va_start(args, pFmt);char buf[BUFFER_LEN] = {};vsnprintf(buf, BUFFER_LEN, pFmt, args);wchar_t wbuf[HALF_BUFFER_LEN] = {};MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, HALF_BUFFER_LEN);info = (std::string)wbuf;va_end(args);}result()->resInfo = info;
}DemoTestResult* DemoTest::result() const
{auto pCurTestInfo = DemoTestManager::instance()->currentTestInfo();if (!pCurTestInfo) {return nullptr;}return pCurTestInfo->result();
}DemoTestResult* DemoTestInfo::result() const
{return const_cast<DemoTestResult*>(&m_pResult);
}
- 我们看到它会将结果记录在
DemoTestInfo
下的m_pResult
对象中。 - 当我们执行槽函数之后,触发测试用例的执行,会到这段逻辑中来:
void DemoTestManager::runTest(const std::string name)
{for (auto pTestInfo : m_testInfos) {if (pTestInfo->name() == name) {pTestInfo->run();m_records[pTestInfo].results.push_back(*pTestInfo->result());showResult(pTestInfo);break;}}
}
-
其中重要的两点是:
1) 记录测试信息m_records[pTestInfo].results.push_back(*pTestInfo->result());
2) 显示结果:
showResult(pTestInfo);
-
showResult()
的主要业务逻辑是将结果相关的信息回显给开发者或者用户,不同的开发者有不同的回显习惯,在此就不在赘述,由开发者自己去实现。
总结
好了,到现在为止,本文和核心内容阐述就完成了,希望能对想自己动手搭建测试框架的朋友有一点建设性的指导意义。如果能在后续工作或学习中有所帮助,那是再好不过。
下一篇文章,我将带大家一起看下,搭建好的测试框架模块(亦或是独立的其它模块)是如何被主模块给加载起来的,感兴趣的朋友可以看下下期文章。