C++服务开发入门指南
  • 序言
  • 前言
  • 一、一个简单的服务
    • 1 什么是服务
    • 2 服务可以用来做什么
    • 3 简单服务框架
  • 二、网络通信服务框架
    • 1 网络服务的基本概念
    • 2 增加监听端口
    • 3 处理客户端会话
    • 小结
  • 三、添加基础模块
    • 1 日志模块
    • 2 定时器
    • 3 事件机制
    • 4 线程池
    • 5 线程安全
    • 小结
  • 四、一个聊天服务
    • 1 需求描述及分析
    • 2 概要设计
    • 3 创建服务项目
    • 4 ClientUser实现
    • 5 RoomMgr实现
    • 6 ChatRoom实现
    • 7 RoomIDMgr实现
    • 小结
  • 五、测试、迭代及重构
    • 1 测试
    • 2 迭代
    • 3 重构
    • 4 版本号
  • 六、架构设计
    • 1 单点服务
    • 2 分布式服务
  • 七、部署及发布
    • 1 部署环境
    • 2 编译环境
    • 3 部署服务
    • 4 发布服务
  • 八、线上问题处理
    • 1 线上问题
    • 2 问题处理
  • 九、程序员的职业规划
    • 职业规划
Powered by GitBook
On this page
  • 开发自测
  • QA测试
  • 线上灰度测试
  1. 五、测试、迭代及重构

1 测试

Previous五、测试、迭代及重构Next2 迭代

Last updated 2 years ago

首先对测试进行简单分类,根据测试人员和测试阶段的不同,可以分为开发自测,QA测试,线上灰度测试。从测试手段上,又可以细分为单元测试,流程测试,压力测试。

开发自测

项目交付前,首要做的是开发自测。其实,测试最好是伴随着开发进行,甚至应该先于开发。

测试如何先于开发?答案是面向接口编程。

先根据要实现的功能,定义好接口函数,函数可以不进行具体实现。

然后再调用接口,编写测试用例,覆盖函数输出的各种情况,进行验证。

这种一般结合单元测试实现,实际开发过程中,一般很难在开发前就做好单元测试,但我们仍建议做好单元测试,即便是在开发完成后。

单元测试

单元测试,是指对软件中的最小可测试单元进行检查和验证,一般是对函数级的验证。

推荐的单元测试工具是GTest,Visual Studio集成了GTest的单元测试模块,可以很方便的为项目设置单元测试工程。

本书中的项目,使用的GTest是从Github上下载的源码,编译成为lib库,加入到项目中。

在testMain.cpp中初始化,并启动测试

#include "gtest/gtest.h"

int main(int argc, char **argv)
{
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

testBuffer.cpp用来测试HYBuffer类的方法

#include "gtest/gtest.h"
#include "HYBuffer.h"

TEST(HYBuffer, Append)
{
    CHYBuffer buf;
    buf.SetHeaderLen(10);
    buf.AppendFormatString("this is test for append string format %d", 100);
    EXPECT_EQ(buf.GetBufLen(), 52);
}

在tests文件夹中单独设置一个CMakeLists,用于编译gtest项目

include(CTest)

set(TEST_BINARY ${PROJECT_NAME}_test)

set(link_GTest 
${PROJECT_SOURCE_DIR}/tests/lib/libgtest.a
${PROJECT_SOURCE_DIR}/tests/lib/libgtest_main.a
)

set(SRC_LIST 
testMain.cpp
testBuffer.cpp
../deps/common/HYBuffer.cpp
)

add_executable(${TEST_BINARY}  ${SRC_LIST})

target_link_libraries(${TEST_BINARY} ${link_GTest})

编译完成会生成一个test程序,执行输出如下

[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from HYBuffer
[ RUN      ] HYBuffer.Append
[       OK ] HYBuffer.Append (0 ms)
[----------] 1 test from HYBuffer (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

单元测试需要不断的补充完善,并根据函数功能的调整做出修改。这是比较繁琐的一个事情,不过比起单元测试的好处来看,还是值得的。

单元测试首先可以验证当前的函数正确性。

更重要的一个作用是,在代码重构的时候,如果修改后的函数可以通过之前的单元测试,则可以认为函数重构过程中没有错误。

因此,单元测试是项目重构正确性的强力支撑。

流程测试

有些情形不太适合做单元测试,比如一个请求发送过来,需要经历一系列的流程处理,比如聊天室服务接收到一个发送消息的请求,需要Session类进行消息读取,传递到ChatUser类,再通过Service调用RoomMgr,转发给ChatRoom进行处理。

这个流程很难使用单元测试进行验证,因为整个流程涉及到好几个类,中间会因为各种判断条件不合预期导致调用链条提前结束,不太容易通过单元测试对结果进行对比验证。

项目开发过程中,前后端可能一起立项,但前端UI细节比较多,一般耗时会比后端更多一些。后端功能开发完成后,如果一直等待前端功能完成后一起连调,往往需要等待很长时间,这在项目开发中一般是不提倡的。服务端需要能够自己提前验证功能。

对于这一类的功能验证,服务端可以开发一个简单的客户端程序,只要求发送相应的消息请求,并接收回复信息。

#include <iostream>
#include <string>
#include "asio.hpp"
#include "Protocol.h"
#include "HYBuffer.h"

void test_tcp_client();
int main()
{
    std::cout << "Start testclient..." << std::endl;
    try
    {
        test_tcp_client();
    }
    catch(std::exception& e)
    {
        std::cout << "exec exception:" << e.what() << std::endl;
    }
    return 0;
}

void RequestSessionID(char buf[])
{
    PHYHEADER pHeader = (PHYHEADER)buf;
    pHeader->dwLength = 0;
    pHeader->wOrigine = 1;
    pHeader->wType = 101;
}

void RequestCreateRoom(char buf[])
{
    PHYHEADER pHeader = (PHYHEADER)buf;
    pHeader->dwLength = 0;
    pHeader->wOrigine = 1;
    pHeader->wType = 102;
}

#define HY_ACK  0x8000

void OnMsg(char*msg, int len)
{
    std::cout << "recv msg " << len << std::endl;
    PHYHEADER pHeader = (PHYHEADER)msg;
    std::cout << "recv msg len:" << pHeader->dwLength <<" ,ori:"<< pHeader->wOrigine << ", type:" << pHeader->wType <<std::endl;
    switch(pHeader->wType)
    {
        case (int)(101 | HY_ACK):
        {
            char * d = msg+HYHEADERSIZE;
            std::string str = d;
            std::cout << "recv msg RequestSessionID " << str<< std::endl;
        }   
        break;
        case (int)(102 | HY_ACK):
        {
            char * d = msg+HYHEADERSIZE;
            std::string str = d;
            std::cout << "recv msg Create Room " << str<< std::endl;
        }
        break;
    }
}

void test_tcp_client()
{
    asio::io_context ioc;
    asio::ip::tcp::resolver resolver(ioc);
    asio::ip::tcp::socket socket(ioc);
    asio::ip::tcp::endpoint ep(asio::ip::address::from_string("127.0.0.1"), 21100);
    socket.connect(ep);
    if(!socket.is_open())
    {
        std::cout << "connect svr failed" << std::endl;
        return;
    }

    int buf_len = 0;
    char buf[256] = {0};
   

    for(int i = 0; i < 200; i++)
    {
        std::cout << "\nplease input cmd "  << std::endl;
        int size = HYHEADERSIZE;
        int nCmd;
        std::cin >> nCmd; 
        switch(nCmd)
        {
            case 0:
                std::cout << "close client..." << std::endl;
                return;
            case 1:
                RequestSessionID(buf);
                break;
            case 2:
                RequestCreateRoom(buf);
                break;
            default:
                RequestSessionID(buf);
                std::cout << "default cmd "  << std::endl;
                break;
        }

        PHYHEADER pHdr = (PHYHEADER)buf;
        std::cout << "send msg len " << size << ", length "  << pHdr->dwLength << std::endl;

        asio::write(socket, asio::buffer(buf, size));


        char buf2[264] = {0};
        buf_len = socket.read_some(asio::buffer(buf2));
        OnMsg(buf2, buf_len);        
    }   
}

测试客户端并不要求功能完备,只要能够简单的模拟发送请求,并查看响应数据即可。

还有一些涉及定时器、事件机制的验证,因为是异步触发,也不适合使用单元测试,但这类的测试可以在服务内部模拟验证。比如,在服务初始化的时候,设置几个定时器,然后从日志中观察定时器的执行情况。

同样,也可以在定时器触发的时候,结合事件触发,查看事件模块的执行情况。

压力测试

在流程测试过程中,模拟单个客户端的请求,可以验证一个请求链条的执行情况。

但服务上线后,会面临大量用户访问的情况,因此有必要进行压力测试。

压力测试具体要测试哪些内容?

首先,需要测试服务的承载上限。对于长连接服务来说,是能够支持多少用户同时在线;对于短连接的服务,是每秒能响应多少次用户请求,即QPS(Queries Per Second)。

了解了服务承载能力,才能在线上运行时根据服务承载能力做好运维。

在测试承载上限的时候,随着压力的逐步增长,一般要关注内存、CPU、流量带宽等指标哪个先达到上限瓶颈。

其次,要验证多用户情况下,服务运行的稳定性。尤其是多线程处理的服务,在多用户的情况下是否会出现死锁或读写脏数据问题。

开发自测需要提前列好自测条目,因为开发人员对所开发的项目是最了解的,哪些是迭代新增功能,哪些老功能受到影响,哪些地方可能存在安全隐患,开发人员都是最清楚的。

功能开发完成后,按照自测条目,逐项进行功能验证,保证交付的产品是符合预期,且没有明显漏洞的。

有些服务开发,测试到这一步就结束了,再由经验丰富的开发主管审核过后,直接就上线发布了。

更多的时候,前端业务也有相应变化,对于用户可感知的修改内容,需要QA测试进一步验证。

QA测试

一般上规模的公司,都有专门的QA团队,对产品功能进行测试。

QA同学会和开发、产品一起进行需求沟通,了解项目迭代进行哪些改动,以便评估测试点,完善测试用例内容,评估测试人力和周期。

QA测试一般是黑盒测试,测试同学不了解程序内部实现逻辑,仅从用户角度查看变化,体验功能。

测试不仅验证正常流程下功能正确性,还需要做一些冲突操作,验证在一些极端情况下,程序运行是否也符合预期。

一个比较好的bug追踪管理工具是禅道,测试同学可以将发现的bug提交到禅道新建的项目中,并指给开发人员进行处理。提交bug时,较好的习惯是,说清楚bug出现的情形,记录复现该bug的操作步骤。

优秀的测试同学,可以从表象分析出问题可能出现的原因,并帮助开发人员快速定位问题原因。

开发收到禅道通知(可以绑定邮箱或即时聊天工具),对bug进行确认,并修复问题,提交新版本,并修改禅道bug状态,由测试同学确认修复结果。

在一个测试周期中,规范的测试流程可能包括第一遍测试,回归测试,验证测试,以及稳定性验收测试。

QA测试保证了上线前,程序是质量可靠的,即便仍存在一些小问题,也不会影响用户体验。

线上灰度测试

经过QA测试验收之后,程序就具备发布的条件了。

不过QA测试毕竟人力和时间有限,不可能测试到所有的问题,有些问题仍需要在真实的生产环境、大量用户访问的情况下才会触发。

正式全网发布是指,线上所有的服务都更新到最新版本。

对于体量较大,用户较多的平台来说,直接进行全网发布风险太大。比如之前的聊天室服务,在全网部署了20台,全部更新后,如果发生问题回退到上一版本,会影响所有用户。因此,有必要引入灰度发布测试。

灰度发布是指,仅在正式环境更新部分(一台或几台)服务,在引入真实用户的情况下,验证服务稳定性。

灰度测试时间一般在3-5天左右,最长不要超过2周。

如果灰度过程中发现问题,可以回退到上一个正式版本,解决完问题后,重新进入灰度发布环节。