美文网首页
测试框架是干什么的?

测试框架是干什么的?

作者: 东方胖 | 来源:发表于2021-12-08 20:40 被阅读0次

    有一段时间,我负责一个语音通讯的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等。

    相关文章

      网友评论

          本文标题:测试框架是干什么的?

          本文链接:https://www.haomeiwen.com/subject/qrntfrtx.html