使用robotframework测试nginx的location指令

Published On March 15, 2017

category server | tags robotframework nginx test


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点:

  1. location分两种,包含修饰符~*(case-insensitive)或~(case-sensitive)的regex location(正则),其他的是prefix location(前缀)
  2. 先检查prefix location,如果prefix location中的字符串是当前请求URI的前缀,则匹配,找到最长前缀匹配的prefix location记录下来(可能没有) 这里有2条特殊的规则:
    1. 如果prefix location包含=修饰符,并且和URI完全匹配,则停止搜索,请求由该location处理;
    2. 如果最长前缀匹配的location包含^~修饰符,则停止搜索,请求由该location处理;
  3. 按顺序regex location定义的顺序在URI中搜索regex location中的正则表达式,如果找到匹配项,则停止搜索,请求直接进入该location处理
  4. 如果没有找到匹配的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_t.conf是robot测试时使用的配置,只保留了最精简的配置,能用默认值的都尽量用默认值,其中event块不能省。 每个location使用return指令返回,并且,返回的body和location后面的参数完全一致(除了个别特殊的)。为了不和正在运行的nginx冲突,使用了88和8888端口,而且设置了不同的pid文件。

将该文件拷贝到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
每个robot文件都是一个test suite,文件名就是该suite的名称,包含suite的文件夹也是test suite,所以test suite可以嵌套。 在robot文件里至少有一个test case,test case写在*** 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
Suite Teardown里通常执行收尾的工作,keyword 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 StatusRun 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/

参考

Robot Framework documentation


qq email facebook github
© 2018 - Xurui Yan. All rights reserved
Built using pelican