Openresty测试框架--Test::Nginx

2022-07-28 09:47:58

Test::Nginx使用说明

1. 简介

1.1 组件介绍

Test::Nginx是一个由Perl编写的nginx测试组件,继承自Test::Base。

1.2 安装方法

sudo cpan Test::Nginx

1.3 测试目录结构

测试目录最好命名为t,因为在测试过程中会在t中创建conf目录存储当前测试的配置和日志(默认路径为t/),应该可以提供参数来指定目录,尚未研究。

cc_cache/
├── go.sh
└── t
    ├── 001-test.t
    ├── 002-strategy.t
    ├── 003-rule.t
    └── ANYU.pm

1.4 测试文件格式

Test::Nginx遵循一种特殊的设计。

#codes...

__DATA__#suites...

如代码片段所示,使用__DATA__将每个测试文件分割成代码和测试用例两部分,Perl解释器将只运行代码部分内容。

__DATA__
其实这是Perl相关的语法,在代码中使用<STDIN>可以从标准输入接收数据,类似地使用<DATA>文件句柄可以直接从脚本自身获取数据(脚本中特殊常量__DATA__之后的内容),同时它也标志着脚本的逻辑代码的结束。
注意:第二次使用句柄时,需要修改偏移量才能重新从头读取数据。

1.4.1 代码部分

# 调用对应的测试模块,一般情况使用'no plan'即可use Test::Nginx::Socket'no plan';# 运行测试用例
run_tests();

1.4.2 用例部分

由一系列测试块(BLOCK)组成,每个测试块由标题、可选描述和一组用例组成。

# 标题名称,合理标题前缀有利于测试结果分析# 可以使用reindex命令自动生成TEST编号前缀=== title TEST01: NAME
optional description# value1包含换行符在内多行数据# 可以使用多个filter对value1进行预处理--- section1(filter...)
value1# value2默认使用了chmop去掉了两边的空白符--- section2: value2

常用sections举例。

=== TEST1: hello, world
This is just a simple demonstration# 增加配置到server{}--- config
location=/t{
    echo"hello, world!";}# 发起测试请求--- request
GET/t# 确认响应body# chomp用来去掉value两边的空白符--- response_body chomp
hello, world!# 确认响应状态码,不显示指定该section时默认200--- error_code:200# 仅运行此block--- ONLY# 跳过此block--- SKIP# eval将value作为代码执行并获取对应结果-- response_bodyeval"a"x4096

1.4.3 更多Sections

Section说明备注
config插入server块配置
http_config插入http块配置
main_config插入顶层配置
request发起请求默认HTTP/1.1,可以后缀指定
pipelined_requests多个请求request好像会自动转换
response_body响应body可以用eval支持qr//格式的正则
response_body_like响应正则response_body好像会自动转换
error_code响应码默认200
more_headers请求头
response_headers响应头检查
error_log错误日志包含
no_error_log错误日志不包含使用[error]确定没异常
grep_error_log筛选日志条件结果与grep_error_log_out匹配
grep_error_log_out匹配筛选日志类res可以是多行内容
wait日志等待时间单位秒,支持float
must_die测试启动失败标记,启动失败则测试通过
ONLY只测试当前block标记
SKIP跳过当前block标记
ignore_response不检查响应完整性默认HTTP/1.1协议格式
timeout连接超时时间默认3秒

测试时将为每个block生成一个配置文件t/servroot/conf/nginx.conf进行测试。
我们还可以通过编写perl代码的方式来新增section或扩展现有的section。

1.4.4 测试代码实例

测试用例文件t/001-test.t中的内容如下所示,以供参考。

12use t::ANYU'no_plan';34 run_tests();567 __DATA__89=== TEST1: test hello world10 Start test::nginx.1112--- config13 location/test{14     rewrite_by_lua_block{15         ngx.print('hello world');16}17}1819--- pipelined_requestseval20[21"GET /test",22"GET /test"23]242526--- response_bodyeval27[28"hello world",29qr/hello.*world/30]313233#--- error_code chomp34#20035

1.5 通用代码封装

通常使用prove来启动每个测试perl脚本来获取测试结果,我们可以将通用的测试启动参数、测试配置和函数封装调用。

-- 封装模块--package t::ANYUTester;# 启动HUP测试模式
BEGIN{if($ENV{TEST_NGINX_CHECK_LEAK}){$SkipReason="unavailable for the hup tests";}else{$ENV{TEST_NGINX_USE_HUP}=1;undef$ENV{TEST_NGINX_USE_STAP};}}# -Base部分在调用t::ANYUTester时传入use Test::Nginx::Socket-Base;

repeat_each(2);
log_level('info');# 控制block测试顺序与代码一致(默认是随机测试)
no_shuffle();# 使用diff风格的字符串对比(response_body/grep_error_log_out等)
no_long_string();# 在每个block启动前调用进行预处理。
add_block_preprocessor(sub{my($block)=@_;# 修改http部分配置my$http_config=$block->http_config;# EOCH为perl的多行字符串语法$http_config.=<<'_EOCH_';
    lua_package_path"/home/work/cc/cc_nginx/luafiles/?.lua;;";
    lua_shared_dict cache_status 100m;
    lua_shared_dict cache_status_lock 100k;
_EOCH_$block->set_value("http_config",$http_config);# 修改server部分配置my$config=$block->config;$config.=<<'_EOCS_';
    location/cc_cache{
        rewrite_by_lua_file/home/work/cc/cc_nginx/luafiles/cc/api/cache.lua;}

    location/rule_check{
        rewrite_by_lua_block{local host= ngx.var.arg_hostlocal module= ngx.var.arg_modulelocal uri= ngx.var.arg_urilocal infos={
                host= host,
                uri= uri,}local rule_api=require"cc.rule.cache"local strategy_api=require"cc.strategy.cache"local key= module..":".. hostlocal rules= strategy_api.lru_get(key)if rules then
                ngx.print(rule_api.match(infos, rules))else
                ngx.print("miss")
            end}}
_EOCS_$block->set_value("config",$config);});1;
__END__-- 主模块--use t::ANYUTester'no_plan';
run_tests();

__DATA__# test suites...

1.6 测试启动器

可以将系统环境变量等启动时需要设置的内容进行封装,避免环境污染,方便操作。

#!/usr/bin/env bashexport PATH=/home/work/cc/cc_nginx/nginx/sbin:$PATH# 部分系统perl环境会提示(Can't locate t/.pm in @INC)export PERL5LIB=$(pwd):$PERL5LIBexec prove"$@"

使用shell命令$ sh go.sh t/进行测试,可以指定某个.t文件进行单独测试。

-v 查看每个测试点的情况
-r 遍历指定的目录

如果使用vim编辑器进行测试用例.t文件的编写,可以直接使用:!prove %快速测试查看结果。

2. Test:Nginx进阶

2.1 知识补充

2.1.1 Perl变量的引用

# codes...our$after_10_min= time()+600;
run_tests();# datas...'prefix "ttl":'.$::after_10_min.'suffix'# datas...

只有在支持eval(作为代码通过perl解释器运行)的section(request/response_body)才支持变量引入,因为eval和run_tests前的代码部分运行很可能不在一个模块内(变量的引入可能在block处理时完成),所以需要使用our定义为包全局变量。

Perl中our和my的区别
my声明的是真正的词法变量,只能在闭合块中访问。
our声明的是包全局变量,在符号表中存储(可以通过全限定在任何地方访问)。
ps. $::var即$main::var,取包全局变量

变量的引用严格使用perl语法,可以通过.连接字符串或在""包闭的串中直接引用。

注意:标准json中是用的双引号"data",不支持’data’

2.1.2 多种lua流程模块

init_by_lua
nginx启动时的配置加载阶段
init_worker_by_lua
worker进程启动时的配置加载阶段
set_by_lua
用于简单的计算设置变量,适用于server/location
rewrite_by_lua
在标准HtpRewriteModule之后进行,适用于http/server/location
access_by_lua
请求访问阶段,在标准HttpAccessModule之后进行,用于访问控制,适用于http/server/location
content_by_lua
内容处理器,接收请求并输出响应,适用于location
header_filter_by_lua
body_filter_by_lua
log_by_lua

参考链接:https://www.cnblogs.com/JohnABC/p/6206622.html

2.1.3 Response为table时的比较

通过在Json请求中增加了一个变量来将预定的结果table传入,然后调用table序列化函数(有序模式),将实际结果和预期结果都转为字符串进行比较。

if data.check_result then
    local check_result = data.check_result
    if is_table(check_result) then
        -- table2string有序将table内数据转为统一格式的字符串
        check_result = util.table2string(check_result, true)
        result = util.table2string(json_decode(result), true)
    else
        check_result = tostring(check_result)
    end

    ngx_log(ngx_ERR, "result: " .. result)
    ngx_log(ngx_ERR, "check_result: " .. check_result)

    if result == check_result then
        ngx_exit(200, 'true')
    else
        ngx_exit(400, 'false')
    end
else
    ngx_exit(200, result)
end

2.1.4 跳过某个.t文件

use Test::Nginx::Socket skip_all=>"some reasons";

2.2 超时测试技巧

2.2.1 连接超时

可以通过在某个IP的服务器链路上的防火墙设置相应的规则,丢弃所有的SYN包,就会产生访问超时的现象。下面是官方示例的代码和相应的"黑洞"地址。

=== TEST1: connect timeout--- config# 配置DNS解析使用的服务器IP
    resolver8.8.8.8;
    resolver_timeout 1s;

    location=/t{
        content_by_lua_block{local sock= ngx.socket.tcp()
            sock:settimeout(100)-- mslocal ok, err= sock:connect("agentzh.org",12345)ifnot ok then
                ngx.log(ngx.ERR,"failed to connect: ", err)
                return ngx.exit(500)
            end
            ngx.say("ok")}}--- request
GET/t--- response_body_like:500 Internal Server Error--- error_code:500--- error_log
failed to connect: timeout

一般情况下在测试中需要把timeout时间相应的缩小,有利于加快测试进度。需要注意的是,Test::Nginx默认的连接超时时间是3秒。

2.2.2 读取超时

可以通过修改server代码,使用sleep等操作长时间不中断连接即可。

=== TEST1: read timeout--- main_config# TCP伪造服务器
    stream{
        server{
            listen5678;
            content_by_lua_block{
                ngx.sleep(10)--10 sec}}}--- config
    lua_socket_log_errors off;
    location=/t{
        content_by_lua_block{local sock= ngx.socket.tcp()
            sock:settimeout(100)-- ms
            assert(sock:connect("127.0.0.1",5678))
            ngx.say("connected.")local data, err= sock:receive()-- try to read a lineifnot data then
                ngx.say("failed to receive: ", err)else
                ngx.say("received: ", data)
            end}}--- request
GET/t--- response_body
connected.
failed to receive: timeout--- no_error_log[error]

2.2.3 发送超时

使用mockeagain配合lua测试代码完成。
参考链接:https://openresty.gitbooks.io/programming-openresty/content/testing/testing-erroneous-cases.html

2.2.4 Client中断

使用timeout配合abort处理,在server中使用sleep延长响应时间,造成客户端中断,观察server日志确定测试结果。

=== TEST1: abort processing in the Lua callback on client aborts--- config
    location=/t{# 检查客户端中止事件
        lua_check_client_abort on;

        content_by_lua_block{# 设置回调函数在Client中断时停止客户端处理local ok, err= ngx.on_abort(function()
                ngx.log(ngx.NOTICE,"on abort handler called!")
                ngx.exit(444)
            end)ifnot ok then
                error("cannot set on_abort: ".. err)
            end

            ngx.sleep(0.7)-- sec
            ngx.log(ngx.NOTICE,"main handler done")}}--- request
    GET/t--- timeout:0.2# 控制Test::Nginx忽略超时错误信息--- abort--- ignore_response--- no_error_log[error]
main handler done--- error_log
client prematurely closed connection
on abort handler called!

2.3 丰富的测试模式

2.3.1 Benchmark模式

在运行prove前设定系统变量TEST_NGINX_BENCHMARK即可开启基准测试。如果使用HTTP/1.1协议,将调用weighttp工具,如果使用HTTP/1.0将会调用ab工具,这些工具需要自己安装的哈。

# 将会发起2000个请求,并发数为2# weighttp -c2 -k -n2000 $url# ab -r -d -S -c2 -k -n2000 $urlexport TEST_NGINX_BENCHMARK='2000 2'
prove t/foo.t

测试结果中可以展示出测试结果供参考。

---weighttp---
finished in 2 sec, 652 millisec and 752 microsec, 75393 req/s, 12218 kbyte/s
requests: 200000 total, 200000 started, 200000 done, 200000 succeeded, 0 failed, 0 errored
status codes: 200000 2xx, 0 3xx, 0 4xx, 0 5xx

---ab---
Time taken for tests:   3.001 seconds
Complete requests:      200000
Failed requests:        0
Requests per second:    66633.75 [#/sec] (mean)
Transfer rate:          10798.70 [Kbytes/sec] received

run_tests()前加入以下命令,可以开启多进程测试。

master_on();
workers(4);

2.3.2 HUP模式

为了保证测试块间的环境隔离,Test::Nginx默认在每个测试块启动独立的Nginx实例。当我们需要在测试块间共享一个实例时(保持共享内存),就需要在测试块之间使用HUP信号的方式切换。

# Blocks间使用HUP方式重启export TEST_NGINX_USE_HUP=1# Files间也使用HUP方式重启export TEST_NGINX_NO_CLEAN=1

也可以在测试文件.t中的代码启动部分使用perl进行处理。

# 使用HUP方式进行BLOCK间的切换测试,共享内存数据等。
BEGIN{# 使用weghttpd/ab+ps进行简单内存泄漏测试if($ENV{TEST_NGINX_CHECK_LEAK}){$SkipReason="unavailable for the hup tests";}else{# 使用HUP方式在block间切换nginx,但是repeat_each时好像不是$ENV{TEST_NGINX_USE_HUP}=1;undef$ENV{TEST_NGINX_USE_STAP};}}use Test::Nginx::Socket'no_plan';# test codes and suites

如果想要在Block内的测试中重新加载openresty,只能通过lua代码来控制了。

local f = assert(io.open("t/servroot/logs/nginx.pid", "r"))
local master_pid = assert(f:read())
assert(f:close())
assert(os.execute("kill -HUP " .. master_pid) == 0)

Nginx的几种信号说明。

kill -HUP nginx进程号("/var/run/nginx.pid")
当nginx接收到HUP信号时,它会尝试先解析配置文件(如果指定文件,就使用指定的,否则使用默认的),如果成功,就应用新的配置文件(例如:重新打开日志文件或监听的套接字),之后,nginx运行新的工作进程并从容关闭旧的工作进程,通知工作进程关闭监听套接字,但是继续为当前连接的客户提供服务,所有客户端的服务完成后,旧的工作进程就关闭,如果新的配置文件应用失败,nginx再继续使用早的配置进行工作。
TERM,INT 快速关闭,关闭信号源(stop)
QUIT 从容关闭,会等待请求结束(quit)
HUP 平滑重启,重新加载配置文件(使用reload会加载两次配置)
USR1 重新打开日志文件,切割日志时用途较大
USR2 平滑升级可执行程序(reopen)
WINCH 从容关闭工作进程(配合USR2进行版本升级)

当然,使用HUP加载的方式会带来一些影响,比如HUP加载会有一个新旧worker交替时间,导致某些情况下测试结果与预期不符。

2.3.3 Valgrind模式

openresty中内存相关问题一般出现在较底层的代码(Nginx/LuaJIT/FFI),Lua代码的内存问题(无限制得对全局表和变量操作)无法使用Valgrind检查,因为它们被LuaJIT垃圾收集机制管理。

export TEST_NGINX_USE_VALGRIND=1

贴一个官方的测试用栗。

=== TEST1: C strlen()--- config
    location=/t{
        content_by_lua_block{local ffi=require"ffi"local C= ffi.C# 避免每次都重新声明这个函数,使用pcall进行验证ifnot pcall(function() return C.strlen end) then
                ffi.cdef[[
                    size_t strlen(const char*s);
                ]]
            end

            local buf = ffi.new("char[3]", {48, 49, 0})
            local len = tonumber(C.strlen(buf))
            ngx.say("strlen: ", len)}}--- request
    GET/t--- response_body
strlen:2--- no_error_log[error]

在直接以valgrind模式进行测试时,会产生一些假性的错误告警,如下所示:

t/a.t .. TEST 1: C strlen()
==7366== Invalid read of size 4
==7366==    at 0x546AE31: str_fastcmp (lj_str.c:57)
==7366==    by 0x546AE31: lj_str_new (lj_str.c:166)
==7366==    by 0x547903C: lua_setfield (lj_api.c:903)
==7366==    by 0x4CAD18: ngx_http_lua_cache_store_code (ngx_http_lua_cache.c:119)
==7366==    by 0x4CAB25: ngx_http_lua_cache_loadbuffer (ngx_http_lua_cache.c:187)
==7366==    by 0x4CB61A: ngx_http_lua_content_handler_inline (ngx_http_lua_contentby.c:300)

这是由于LuaJIT的内存优化导致的,在LuaJIT的代码仓库中有一个文件lj.supp列出了所有已知的valgrind会产生的误报,我们可以直接复制到工作目录并重命名为valgrind.suppress供测试模式调用valgrind使用。

cp -i /path/to/luajit-2.0/src/lj.supp ./valgrind.suppress

再次运行测试时,将会收到正确的测试结果。

t/a.t .. TEST 1: C strlen()
t/a.t .. ok
All tests successful.
Files=1, Tests=3,  2 wallclock secs ( 0.01 usr  0.00 sys +  1.51 cusr  0.06 csys =  1.58 CPU)
Result: PASS

在测试过程中还可能会出现nginx内核和openssl库等相关的误报,一般情况下框架会自动识别并在测试结果结尾展示可以添加到valgrind.suppress中的内容,如下所示。

{
   <insert_a_suppression_name_here>
   Memcheck:Addr4
   fun:str_fastcmp
   fun:lj_str_new
   fun:lua_setfield
   fun:ngx_http_lua_cache_store_code
   fun:ngx_http_lua_cache_loadbuffer
   fun:ngx_http_lua_content_handler_inline
   fun:ngx_http_core_content_phase
   fun:ngx_http_core_run_phases
   fun:ngx_http_process_request
   fun:ngx_http_process_request_line
   fun:ngx_epoll_process_events
   fun:ngx_process_events_and_timers
   fun:ngx_single_process_cycle
   fun:main
}

如果要测试一些由于lua分配的对象导致的内存泄漏情况,我们需要在编译luajit的时候加入参数关闭luajit的内存分配策略。

make CCDEBUG=-g XCFLAGS='-DLUAJIT_USE_VALGRIND -DLUAJIT_USE_SYSMALLOC'

或者编译openresty的时候加入特定参数。

./configure \
    --prefix=/opt/openresty-valgrind \
    --with-luajit-xcflags='-DLUAJIT_USE_VALGRIND -DLUAJIT_USE_SYSMALLOC' \
    --with-debug \
    -j4make -j4sudomakeinstall# 或者openresty提供了一个更为直接的参数来编译关闭内存管理的openresty版本
--with-no-pool-patch

nginx1.9.13之后的版本,提供了一个宏用来设置实现类似"无池补丁"的效果,但是效果并没有编译时关闭内存管理那么干脆利落。

#define NGX_DEBUG_PALLOC

Valgrind无法识别堆栈和应用级别的内存问题。
Google团队开发的AddressSanitizer也可以用来识别内存泄漏,但各有千秋。

2.3.4 CheckLeak模式

Valgrind非常擅长检测各种内存泄漏和内存无效访问,但在面对应用程序级的垃圾收集器(GC)和内存池等内存管理器中的泄漏方面也受到限制,这在现实中很常见。所以Test::Nginx::Socket中新增了一种测试模式来检测内存问题。

export TEST_NGINX_CHECK_LEAK=1

这种测试模式下,框架将会周期性的使用ps命令来获取当前占用的系统内存大小,计算出一个拟合的斜率k表示内存的增长速度,可以通过k来确认是否有内存泄漏问题出现。通过在Lua中定期调用垃圾收集函数,可以确保k参数的有效性。

collectgarbage()

然而,这种测试模式存在一个很大的缺点,它无法提供有关泄漏(可能)发生的位置的任何详细信息。它所报告的只是数据样本和其他指标,只能验证是否存在泄漏(至少在某种程度上)。

在Wiki中应该还有更多的模式来解决这个问题,等有时间去研究一下。

3. Wiki学习链接

使用说明:https://openresty.gitbooks.io/programming-openresty/content/
详细内容:https://metacpan.org/release/Test-Nginx

  • 作者:醇雾
  • 原文链接:https://blog.csdn.net/wuchunlai_2012/article/details/100059067
    更新时间:2022-07-28 09:47:58