有一段时间,我负责一个语音通讯的SDK的集成自动化测试。
有别于单元测试的小粒度的测试,这种类型的测试一个SDK的对外接口可能涉及几十甚至上百个个函数的联动运行,但这不是这个级别的测试关心的事情。我只需要针对大约几十个对外的api进行调用,运行,验证返回值就行。
当时没有做过多的思虑,以为测试运行,断言和报告是比较容易的事情,我应该把精力放在跨平台,测试协作机,异步接口测试和环境构建这些问题上面。这些问题自然也是很重要,不过我可能低估了断言,报告驱动这些看起来很初级的东西。
在写代码过程中,我慢慢发现我写了很多的if-else
比如
HHErrorCode code = engine->setUserRole(USER_TALKER_FREE);
if (code == HHSuccess) {
//输出一个Pass标记汇总到一个数据容器中,以便最后被测试报告收集
} else {
//测试失败,我要打印一些信心,基本的比如,实际返回的错误码是XXX,而预期的错误码是YYY
}
//测试另一个参数
HHErrorCode code2 = engine->setUserRole(USER_TALKER_MIC);
....
//这里校验结果仍然需要一堆if-else ....
于是我自然的想到用宏把这些长得有点像的代码进行替换啦。
最后我的测试代码长得像这样:
EXPECT_EQ("设置用户说话时的角色", "setUserRole", code, HH_SUCCESS);
比if-else简洁了很多,出错时也会按照固定范式打印出一些错误信息,但是还是有些毛病,
- 参数太多,这种宏压根不敢给别人用,你得解释很多,第一个参数是什么,第二个参数是什么,第三个参数是什么吗...
- 看看我的"宏"是怎么实现的,其实不是真正的宏,是一个函数,前面说是宏是为了对应主流的测试框架,因为这里做的事情和框架的断言宏是类似的。
这个断言函数做了很多,如果要适用于更多场景,它有很大的问题
首先,它假设了expect和actual都是T类型,并且可以用 == 号比较。
我们知道,这未必。等号还可能存在一些陷阱。因此使用时比较多加小心,万一数据类型不能比较或者比较的不是值而是指针怎么办?我们必须把返回的结果“值化”,把它弄成可以比较的东西才可以用这个函数
template <class T>
static void EXPECT_EQ(const std::string&casename, const std::string & suitname,
T actual, T expect,
const std::string& comment=std::string())
{
//std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);
mtx.lock();
std::string message;
std::stringstream ss;
ss << actual;
std::string actual_str = ss.str();
ss.str("");
ss << expect;
std::string expect_str = ss.str();
std::string placehold(' ', 8);
if (casename.length() < 32) {
for (std::string::size_type i = 0; i < casename.size(); ++i){
placehold[i] = casename[i];
}
}
std::string result;
if (actual == expect) {
result = "\t"
"[ <font color=\"green\">OK</font> ]";
} else {
result = cocos2d::StringUtils::format("\t"
"[ <font color=\"red\">Failed</font> ]"
" ======= Expect is %s"
", But actual is %s", expect_str.c_str(), actual_str.c_str());
}
std::string row = cocos2d::StringUtils::format("<tr>"
"<td>%d</td>"
"<td>%s</td>"
"<td>%s</td>"
"<td>%s</td>"
"<td>%s</td>"
"</tr>", CaseNum++, suitname.c_str(),
casename.c_str(), result.c_str(),
comment.c_str());
results.push_back(row);
mtx.unlock();
write_a_message(row);
}
- 此外我做了很多格式化,最终都是为了报告
以上的实现非常ugly,时隔多年再次看到简直是不忍卒睹。
但是通过这个例子我们大概知道了,我们在写测试用例的时候,需要干很多“通用”的事情。
比如,上面的例子中,断言,如果你重新写断言函数或者C++的宏,显然你需要考虑很多:
- 基本的判断,如预期值和实际值是否相等
- 如果是对象地址判断呢,也许需要另一个函数
- 布尔值字符串,像上面我写的那个断言函数,在判断const char*类型的时候会发生车祸(为什么?)为此,我又写了这个函数予以矫正
static void EXPECT_EQ_STR(const std::string&casename,
const std::string& suitname,
const char* actual,
const char* expect,
const std::string& comment = "") {
EXPECT_EQ(casename, suitname, std::string(actual), std::string(expect), comment);
}
这样的东西显然不够精简
- 测试报告,我们需要将每条测试结果写到报告中,最终呈现给哟用户进行及时反馈。报告的内容稍微想想,其实也有许多的内容,测试耗时,错误信息,执行时间戳,用例归属人,以及测试用例的信息树(所属的suit,模块等等)
- 测试驱动和用例管理
- 测试前置和后置
再看我的测试用例的某一个类,这个类包含一组测试的初始化,如事件注册,组件的初始化。然后将每条用例放到private控制的标号底下,在公共接口runtests中运行所有用例。
当然,如果要增加删减用例的话,不得不修改这个类,在private:下面增加一个函数,然后把这个函数加到runTests函数下面.... 这不是template模板方法的design pattern,虽然有点像,但它不是,毕竟每个用例不是virtual,我也无法预估会有多少用例不断被加进来。
class TalkCasesController : public TalkCallbackWrapper
{
public:
void init();
TalkCasesController();
~TalkCasesController();
void startTest();
void onEvent(const HHEvent event, const HHErrorCode error,
const char *channel, const char * param) override;
void onPcmData(int channelNum,
int samplingRateHz,
int bytesPerSample,
void* data,
int dataSizeInByte) override;
virtual void onRequestRestAPI( int requestID,
const HHErrorCode &errorCode,
const char* strQuery,
const char* strResult ) override;
virtual void onBroadcast(const HHBroadcast bc, const char* channel, const char* param1, const char* param2, const char* strContent) override ;
// ... 此处还有很多onXXXX的虚回调函数的实现
void runTests();
void actionInputData();
void sendemail();
std::string getRobotUser() {
return m_robotuser;
}
private:
void actionUninit();
void actionInit();
void actionJoinSingleRoom();
void actionLeaveSingleChannel();
void actionJoinMultiRoom();
void actionLeaveMultiMode();
void actionJoinRoomZhubo();
void testThreadFunc();
void testTalkMode(); //通话接口 音量调节等等
void testFreeVideoMode();
void testHostMode();
private:
std::string m_robotuser; //协作机的id
std::condition_variable m_cv;
std::mutex m_mutex;
std::thread _run_thread;
};
另外我在 runTests 函数中没有做什么事情,它只是把用例堆在一起,一起运行。
如果其中有用例发生阻塞或中断,什么也没管。
以上大概解释了为什么要使用测试框架的原因,如果你不用,你需要自己做更多事情,这些事情不简单而且很容易出错,我在自己写测试用例的时候,把那些通用公共的部分抽象出来,它就会变成一个“测试框架”的雏形。
总结:
一个测试框架的作用,就是帮助你完成那些通用的事情,测试断言,测试报告,测试用例管理,测试运行驱动等等。当然仅仅包含这些功能的框架,它还是比较原始的。
有影响力的测试框架往往包含了更多的内容,例如数据构造(mock),benchmark的用例支持,测试固件,以及产品层面的特性,例如易用性,移植性等等。C++目前最流行的单元测试框架有GoogleTest,CppUnit等。
网友评论