首先对测试进行简单分类,根据测试人员和测试阶段的不同,可以分为开发自测,QA测试,线上灰度测试。从测试手段上,又可以细分为单元测试,流程测试,压力测试。
开发自测
项目交付前,首要做的是开发自测。其实,测试最好是伴随着开发进行,甚至应该先于开发。
测试如何先于开发?答案是面向接口编程。
先根据要实现的功能,定义好接口函数,函数可以不进行具体实现。
然后再调用接口,编写测试用例,覆盖函数输出的各种情况,进行验证。
这种一般结合单元测试实现,实际开发过程中,一般很难在开发前就做好单元测试,但我们仍建议做好单元测试,即便是在开发完成后。
单元测试
单元测试,是指对软件中的最小可测试单元进行检查和验证,一般是对函数级的验证。
推荐的单元测试工具是GTest,Visual Studio集成了GTest的单元测试模块,可以很方便的为项目设置单元测试工程。
本书中的项目,使用的GTest是从Github上下载的源码,编译成为lib库,加入到项目中。
在testMain.cpp中初始化,并启动测试
Copy #include "gtest/gtest.h"
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
testBuffer.cpp用来测试HYBuffer类的方法
Copy #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项目
Copy 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程序,执行输出如下
Copy [==========] 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细节比较多,一般耗时会比后端更多一些。后端功能开发完成后,如果一直等待前端功能完成后一起连调,往往需要等待很长时间,这在项目开发中一般是不提倡的。服务端需要能够自己提前验证功能。
对于这一类的功能验证,服务端可以开发一个简单的客户端程序,只要求发送相应的消息请求,并接收回复信息。
Copy #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周。
如果灰度过程中发现问题,可以回退到上一个正式版本,解决完问题后,重新进入灰度发布环节。