location是nginx里最重要的指令,也是我们在使用nginx时最常接触的指令。nginx的大部分配置都是在location里进行,因此,熟练掌握location的匹配规则尤为重要。本篇文章使用robotframework来测试location指令,主要是为了演示robotframework的用法。因为一个nginx模块给用户提供的接口就是指令或变量,所以当我们自己开发一个nginx模块,也可以用相同的方法进行测试。
本文的代码可以从github:yanxurui/robot-location 下载。
自动化测试
为什么需要自动化测试?
想一下,如果没有robotframework这样的自动化测试工具,我们是怎么做的?我们会在命令行里使用curl发送http请求,检查响应是否符合预期。自动化测试并不会比这种方式提供更多的测试功能,它只不过是把手动测试的过程变成代码。在需要的时候重复执行,也就是回归测试,可以确保代码修改后没有影响原来的功能。正因如此,github上有出名的项目都包含专门的测试代码。自动化测试常常由持续集成工具比如jenkins来触发。
robotframework简介
robotframework是最初由诺基亚的一个部门开发的一种通用的acceptance测试工具,acceptance test是最高层次的测试,站在用户或客户的角度按照产品需求进行功能验收,其他测试包括functional test和unit test。作为一种通用的测试框架,robotframework的功能很多,可以胜任几乎所有的自动化测试任务。它robotframework是用python写的一个库,使用方式是按照它特有的语法(tabular sytax)写test case然后使用robot命令运行。它的特点是keyword-drive,除了配置外,test case由很多keyword组成,keyword其实就相当于函数,可以接受参数,可以有返回值。robot本身提供了很多的keyword可以满足大部分测试需求,我们也可以用robot的语法自定义keyword,或在python里创建新的keyword。不仅有命令行输出,也包括report和log html文件以及程序可读的xml文件。
我所在的项目组使用robotframework对cdn的缓存进行测试。
location指令
匹配规则
根据location指令的文档可以将location的匹配规则总结成以下4点:
- location分两种,包含修饰符~*(case-insensitive)或~(case-sensitive)的regex location(正则),其他的是prefix location(前缀)
- 先检查prefix location,如果prefix location中的字符串是当前请求URI的前缀,则匹配,找到最长前缀匹配的prefix location记录下来(可能没有)
这里有2条特殊的规则:
- 如果prefix location包含=修饰符,并且和URI完全匹配,则停止搜索,请求由该location处理;
- 如果最长前缀匹配的location包含^~修饰符,则停止搜索,请求由该location处理;
- 按顺序regex location定义的顺序在URI中搜索regex location中的正则表达式,如果找到匹配项,则停止搜索,请求直接进入该location处理
- 如果没有找到匹配的regex location,则请求由第2步记录的最长前缀匹配的prefix location处理,如果第2步没有找到,则报404。
嵌套location
location是可以嵌套的,如果多个location具有公共的配置,除了用include的方式外,还可以把公共的配置放到父location里,需要单独配置的放到子location里。
关于location嵌套,文档中并没有过多提及,所以规则不是很清楚。
当uri匹配父location,子location的匹配同样针对用户请求的uri,并不是 去掉父location的前缀后进行比较。
只有prefix location可以嵌套其他location(prefix或regex location),如果regex location包含了prefix location会报错is outside location
。
进入了子location后,在父location中位于该子location后面的配置也会生效,因为指令是按phase进行的,不是按我们看到的顺序执行的。
代码解释
nginx的配置
pid logs/nginx_t.pid;
events {
}
http {
# variable $dollar is $
# this is the only way of printing string which contains $
geo $dollar {
default "$";
}
server {
listen 88;
server_name localhost;
location / {
return 200 '/';
}
## prefix locations
location /home {
return 200 '/home';
}
location /home/foo {
return 200 '/home/foo';
}
location /home/foo/images {
return 200 '/home/foo/images';
}
location /tmp/ {
proxy_pass http://127.0.0.1:8888;
}
## regex locations
location ~ baz {
return 200 '~ baz';
}
location ~* /a\Wb {
return 200 '~* /a\Wb'; # \W can match a space between a and b
}
location ~* ^/insensitive$ {
return 200 '~* ^/insensitive$dollar';
}
location ~ ^/dev/sd([a-z])([1-9]*)$ {
return 200 '$1:$2';
}
location ^~ /etc {
return 200 '^~ /etc';
}
## for exact
location = /etc {
return 200 '= /etc';
}
location /etcetera {
return 200 '/etcetera';
}
location ~ /et[a-z] {
return 200 '~ /et[a-z]';
}
## nested location
location /var {
add_header Var-In 'hi';
location /varia {
return 200 '/varia';
}
location ~ ^/var(\d)$ {
return 200 '/var:$1';
}
add_header Var-Out 'bye';
return 200 '/var';
}
}
# another virtual host using mix of IP-, name-, and port-based configuration
server {
listen 8888;
location / {
return 200 'I am listening 8888';
}
}
}
将该文件拷贝到nginx的conf目录下,并且启动/关闭nginx的任务已经写成了脚本,由robot的准备/收尾阶段执行,不需要我们手动操作。
robot文件的结构
*** Settings ***
Documentation test the location directive in nginx
... Almost all locations return the same string configured after location directive
... In robot \ is an escape character, it requires escaping it with an other backslash like \\
Library Collections
Library OperatingSystem
Library RequestsLibrary
Suite Setup Start Nginx Or Reload Config
Suite Teardown Run Keyword If ${do_post} Stop Nginx
*** Variables ***
${URL}= http://127.0.0.1:88
*** Test Cases ***
URI Should Be Decoded
[Documentation] decoding the text encoded in the “%XX” form
[Template] Send Request And Verify Response
/a b ~* /a\\Wb
/a%20b ~* /a\\Wb
URI Should Be Resolved
[Documentation] resolving references to relative path
[Template] Send Request And Verify Response
/home/./foo /home/foo
/home//foo /home/foo
/home///foo /home/foo
/home/bar/../foo /home/foo
The Longest Matching Prefix Is Used If No Regex matches
[Tags] prefix
[Template] Send Request And Verify Response
/home/fo /home location /home is the prefix of uri /home/fo, but location /home/foo isn't
/home/foo /home/foo
/home/foo/ /home/foo
The Longest Matching Prefix Is Used If It Has ^~ Modifier
[Tags] prefix
[Template] Send Request And Verify Response
/etcet ^~ /etc
Case Sensitive
[Documentation] matching with prefix strings is case sensitive on linux.
... For case-insensitive operating systems such as macOS and Cygwin,
... matching with prefix strings ignores a case.
... This test is run on linux
[Template] Send Request And Verify Response
/home/Foo /home
/home/FOO /home
Regex Match
[Tags] regex
[Template] Send Request And Verify Response
/baz ~ baz
/home/baz ~ baz
/tmp/iambazille ~ baz
Regex Match Case Insensitive
[Tags] regex
[Template] Send Request And Verify Response
/insensitive ~* ^/insensitive$
/Insensitive ~* ^/insensitive$
/INSENSITIVE ~* ^/insensitive$
Regex With Capture
[Documentation] Regular expressions can contain captures
... that can later be used in other directives.
... The first capture is $1
[Tags] regex
[Template] Send Request And Verify Response
/dev/sda a:
/dev/sda1 a:1
/dev/sdb9 b:9
/dev/Sda /
Exact Match
[Documentation] If an exact match is found, the search terminates
[Tags] prefix
[Template] Send Request And Verify Response
/etc = /etc ^~ /etc is before = /etc but exact match has highest privilege
/eta ~ /et[a-z]
/etcetera ~ /et[a-z] longest matched location is /etcetera, but still search regex location
Nested Locations
[Documentation] If an exact match is found, the search terminates
[Tags] nested
[Template] Send Request And Verify Response
/var /var
/variable /varia
/var1 /var:1
/var9 /var:9
Special Redirect
[Timeout] 1s
${resp}= Send Request And Verify Response /tmp I am listening 8888
Should Be Equal As Strings ${resp.history[0].status_code} 301
Dictionary Should Contain Item ${resp.history[0].headers} Location ${URL}/tmp/
*** Keywords ***
Send Request And Verify Response
[Documentation] Request to http://127.0.0.1${uri} should be handled by location which returns ${body}
[Arguments] ${uri} ${body} ${description}=${EMPTY}
${passed}= Run Keyword And Return Status Should Not Be Empty ${description}
Run Keyword If ${passed} Log ${description} console=True
Create Session nginx ${URL}
${resp}= Get Request nginx ${uri}
Should Be Equal As Strings ${resp.status_code} 200
Should Be Equal As Strings ${resp.text} ${body}
[Return] ${resp}
Start Nginx Or Reload Config
Set Environment Variable NGX_DIR ${ngx_install_dir}
${rc} ${output}= Run And Return Rc And Output sh -ex pre.sh
Log To Console ${output}
Should Be Equal As Strings ${rc} 0
Stop Nginx
${rc} ${output}= Run And Return Rc And Output sh -ex post.sh
Log To Console ${output}
Should Be Equal As Strings ${rc} 0
*** test cases ***
下面,叫做test case table。除此之外还可以包含:
- setting table: 设置该suite执行前的准备动作和执行后的收尾动作,引入library(包含keywords的python 模块),variable(包含变量的python模块)或resource(包含keyword或variable的其他robot文件)。上面的代码引入了Collections和OperatingSystem两个标准库和RequestsLibrary库,它们包含下面将要使用到的keywords。
- variable table: 定义该suite范围内的变量,
${URL}
是nginx监听的地址 - keyword table: 定义该suite范围内的keyword
table中的元素使用|
或至少两个空格作为分隔符,通常为了便于阅读都是使用4个空格,为了达到对齐的效果,常常会使用更多的空格。
setup and teardown
Suite Setup和Suite Teardown分别是整个suite在在运行之前和之后要执行的keyword。如果Suite Setup失败,后面的test case统统标记为失败,并且不会执行。 无论如何,最后都会执行Suite Teardown里的keyword,如果Suite Teardown失败,前面所有的test case都会被标记为失败。我们利用这两个设置来启动和关闭nginx,nginx的启动与停止更适合写成shell脚本,然后在robot里使用OperatingSystem library提供的keyword运行。
在Suite Setup里执行Start Nginx Or Reload Config
,它首先使用变量${ngx_install_dir}
设置环境变量NGX_DIR
,然后运行sh -ex pre.sh
。
-e表示遇到执行失败的命令(返回值不为0)就停止,-x表示输出执行的命令。
在pre.sh脚本里,首先检查pid文件是否存在,如果存在则reload,否则start。
# NGX_DIR is an environment variable
if [ ! ${NGX_DIR} ]
then
echo environment variable NGX_DIR not found
exit 1
fi
NGX_BIN=${NGX_DIR}/sbin/nginx
NGX_CONF=${NGX_DIR}/conf
cp nginx_t.conf ${NGX_CONF}
if [ -s ${NGX_DIR}/logs/nginx_t.pid ]
then
${NGX_BIN} -c ${NGX_CONF}/nginx_t.conf -s reload
else
${NGX_BIN} -c ${NGX_CONF}/nginx_t.conf
fi
Stop Nginx
根据变量${do_post}
决定是否要停止nginx。实际项目中的post.sh可能比这里复杂的多:
${NGX_DIR}/sbin/nginx -c ${NGX_DIR}/conf/nginx_t.conf -s stop
Test Cases
Test Cases下面包含了所有的测试用例,请看最后一个,Special Redirect
是该test的名字,它包含setting和keyword两部分,我们使用了Timeout这个设置,如果运行时间超过1s,就认为失败。剩下的每行是一个keyword,keyword可以是builtin库或通过Library引入的库(可以递归)或keyword table里定义的keyword。我们调用了自定义的keyword并传递了两个参数,同时还接收了返回值,keyword本质上就是函数。
Send Request And Verify Response
这个keyword使用RequestsLibrary库向制定的地址发送Get请求,然后检查status code和body。${URL}
是variable table中定义的一个变量。Run Keyword And Return Status
和Run Keyword If
的组合通常用来根据某个keyword执行的结果判断是否要执行另一个keyword,Should Be Equal As Strings
比较两个字符串,如果不是字符串,会自动转化。Log
用来输出日志。这些都是builtin的keyword。
其他的test case使用了一种特殊的语法,叫做data-driven style的test cases,在相同的流程下测试不同的输入输出非常有帮助。[Template]指定运行的keyword是Send Request And Verify Response
,下面的每一行代表一组输入数据,这样的话一个test case里包含了很多小的case。
运行测试
robot framework是以命令行的方式运行测试,robot命令有很多参数,通常会放到一个文件中,然后使用argumentfile参数指定该文件。并且在该参数后面添加的命行行参数会覆盖argumentfile里面的同名参数(支持多次使用的参数除外)。
robot --argumentfile args.in
args.in的内容如下:
--outputdir output
--variable ngx_install_dir:/opt/nginx
--variable do_post:True
--consolemarkers off
location.robot
- —-variable用来设置全局变量,变量名和值之间用:分隔。ngx_install_dir设置nginx的安装路径,默认是/opt/nginx,你需要设置成你自己的路径,do_post前面已经讲过了。
- consolemarkers设为off是robot framework的作者建议的,否则console输出有时候会有问题,可以看这里dotted console interrupts logs formating。
- 最后一个参数是要运行的test suite(robot文件或包含robot文件的目录)。
查看日志
命令行的输出提供了一个简单直观的结果,robot也提供更加详细的日志。robot运行后会输出三个文件:
- output.xml: machine readable XML format for post processing. Log and report files are generated based on it.
- log.html: details about executed test cases.
- report.html: overall test execution status.
如果你是在linux上运行的,使用下面的方式可以在本机的浏览器上查看日志:
cd output
python -m http.server 8000
# python -m SimpleHTTPServer 8000 # if you use python2
http://<your-ip>:8000/