Fork me on GitHub

Robot Framework练习之一——自动化Redmine操作

目标分析

既然是练习,那就从最基本的开始,实现登(登录)增删改功能。用例流程如下:

  1. 登录
  2. 新建issue
  3. 修改issue(增加图片)
  4. 删除issue

难点

  • token传递
  • 上传图片

代码结构

一段完整的RF(Robot Framework简称)代码一般包含四部分(都为可选):

*** Settings ***                                    # 基本配置
Documentation     The documentation of this test suite.     
Suite Setup       keyword                           
Suite Teardown    keyword                           
Test Setup        keyword                           
Test Teardown     keyword                           
Library           RequestsLibrary                                   

*** Variables ***                                   # 初始化一些变量
${user}           username
${pw}             123456

*** Test Cases ***                                  # 要执行的用例(注意缩进)
test case1
    xxx
    ...

test case2
    xxx
    ...
...

*** Keywords ***                                    # 自己定义的关键字
key word1
    xxx
    ...
key word2
    xxx
    ...
...

用到的关键字及对应包

BuiltIn

String

RequestsLibrary

具体实现

登录

首先用浏览器工具抓包,查看登录POST参数,大概是下面这样的:

utf8: 
authenticity_token: qM9e18uN+EHFabwx2vDJtY10cldrFwtBLbojTdhvii8WeDKHsrVNf+4uMWBpskejW0Wty1gaP+GI81sRJkzudQ==
back_url: http://xxx.redmine.com/redmine/
username: username
password: 123456
login: 登录 »

其中authenticity_token就是我们要从上一个页面获取的。

其实上一页面的请求地址是什么根本无关紧要,关键在于token传递的连续性,即在同一域名下,上一次请求生成的token传递到当前请求,并在当前请求生成一个新的token,可以用来传递到下一请求。token的连续性作为判断是否同一会话(session)的依据。

查看上一页面html,前几行如下:

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<title>Redmine</title>
<meta name="description" content="Redmine" />
<meta name="keywords" content="issue,bug,tracker" />
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="KJG1UYRS8rOMnlsXOzH41cZtxJkrt9cbO69z3iWrX8G1dRKiIlOOjm5HfQNJLX2StLo/4dS5Jn3Q5FSzX2MMNA==" />
...

考虑到数据不是json格式,所以决定使用万能的正则表达式,可以放到变量初始化里面:

*** Variables ***
${tokenReg}       name="csrf-token" content="(.*?)"

因为要实现会话控制,所以我们在具体请求之前,要先创建一个session,这里我们可以把headers一起封装:

${headers}    Create Dictionary    User-Agent=value1    Accept=value2    ...
Create Session    redmine    http://xxx.redmine.com    headers=${headers}

注意:${headers}里不要封装Content-Type,可能会导致传参错误。

现在,发送第一个请求,并获得token:

${resp}    get Request    redmine    /
# 这里可以有2种方法,方法1返回值是list,包含所有匹配项:
${tokenList}    Get Regexp Matches    ${resp.text}    ${tokenReg}    1
${token}    set variable    ${tokenList}[0]
# 方法2:调用python re库方法,因为使用了group()方法,返回值直接是string:
${token}    evaluate    re.search(r'${tokenReg} ','''${resp.text}''').group(1)    re

第二步,封装参数,执行登录请求:

${params}    Create Dictionary    utf8=    authenticity_token=${token}    username=${user}    password=${pw}    login=登录 »
${resp}    Post Request    redmine    /redmine/login    params=${params}

登录成功的响应页面会带上用户名,这里可以加个验证:

Should Contain    ${resp.text}    ${user}

整个登录操作就这样实现了。因为后续的所有操作都是建立在登录的基础上,所以当我们测试非登录功能时,可以将登录操作封装为关键字,在Suite Setup时执行此关键字。同理可以在Suite Teardown中执行登出。完整代码见文章最后。

新建issue

同样先抓包,获取POST参数,可以看到这里仍然需要传递token,为了方便,将获取token封装为关键字:

*** Keywords ***
get token
    [Arguments]    ${tokenReg}
    ${resp}    get Request    redmine    /
    ${tokenList}    Get Regexp Matches    ${resp.text}    ${tokenReg}    1
    [Return]    ${tokenList}[0]

代码原理和登录一样,封装参数,执行请求。参数有点多,完整代码见文章最后。

*** Test Cases ***
add issue
    ${token}    get token
    ${params}    Create Dictionary    authenticity_token=${token}    ...
    ${resp}    Post Request    redmine    /redmine/projects/automatic/issues    params=${params}

参数中有中文或特殊字符不用另外处理,请求的时候会自动url转码的。

修改issue(增加图片)

修改需要解决两个问题,一是动态传参问题,另一个是上传图片的问题。

先讲第一个问题,首先在分析POST参数时候,发现比新增多了几个字段:lock_versionlast_journal_id,这2个是可以从问题详情页面获取的。其中lock_version最重要,用于issue的版本控制,起始为0,每成功修改1次加1,这样可以确保当前issue的当前版本只能被1个人修改。last_journal_id是上一条操作记录的id,初始为空值。知道原理就好办了,先查看问题详情页面,写出对应的正则表达式:

*** Variables ***
${lock_versionReg}    value="(.*?)" name="issue\\[lock_version\\]"
${last_journal_idReg}    id="last_journal_id" value="(.*?)"

注意这里[]要用2个\\转义,1个\是用来转义另一个\的。

由于正则匹配不光token要用到,所以我又封装了另一个关键字get match

*** Keywords ***
get match
    [Arguments]    ${reg}    ${text}
    ${match}    Get Regexp Matches    ${text}    ${reg}    1
    [Return]    ${match}[0]

新建的issue还没有任何修改,${last_journal_idReg}是获取不到匹配的,所以这里要加个if判断:

${lock_version}    get match    ${lock_versionReg}    ${resp.text}
${last_journal_id}    Run Keyword If    '${lock_version}'!='0'    get match    ${last_journal_idReg}    ${resp.text}    ELSE    set variable    ${null}

${null}${None}均表示空,注意大小写。

第一个问题解决了,那么来看第二个问题。上传文件的思路是先将文件转换成二进制流,然后将二进制流作为POST参数上传。一开始我打算用OperatingSystem库里的Get Binary File关键字(图片在用例同一目录下):

${file_data}    Get Binary File    ${CURDIR}${/}pic.png
${files}    Create Dictionary    attachments[1][file]=${file_data}

但是这样上传的图片是没有名称的,使用默认的attachments[1][file],最关键的是不带后缀名。这个问题后面再想办法解决吧。现在换一种思路,使用python方法,完美实现:

${file_data}    evaluate    open('./pic.png','rb')
${files}    Create Dictionary    attachments[1][file]=${file_data}

好了,现在到了最关键的地方,如何传参${files}是放到${params}里一起传,还是两个分开传?${headers}里不要加Content-Type=multipart/form-data

TODO:Google有不少地方都都有说${headers}里不要写Content-Type,但是没有讲为什么,我一开始也在这里被坑了好久。然后传参的时候,各种组合都不行,但是最后还是试出来了。${files}${params}要分开传,而且不能用params=${params},要用data=${params}

${resp}    Post Request    redmine    /redmine/issues/16850    data=${params}    files=${files}

可以参考Robotframework.request - How to make a POST request with content “multipart/form-data”

删除issue

删除issue就没什么技术含量了,不过这里传参仍然要用data=${params}

完整代码

仅对用户名、密码,Redmine地址做了修改。

*** Settings ***
Documentation     an example of redmine exercise.
Suite Setup       setup redmine
Suite Teardown    Delete All Sessions
Library           RequestsLibrary
Library           String

*** Variables ***
${user}           username
${pw}             123456
${tokenReg}       name="csrf-token" content="(.*?)"
${lock_versionReg}    value="(.*?)" name="issue\\[lock_version\\]"
${last_journal_idReg}    id="last_journal_id" value="(.*?)"

*** Test Cases ***
add issue
    ${token}    get token
    ${params}    Create Dictionary    utf8=    authenticity_token=${token}    issue[is_private]=0    issue[tracker_id]=4    issue[subject]=robot
    ...    issue[status_id]=7    was_default_status=7    issue[priority_id]=13    issue[assigned_to_id]=154    issue[fixed_version_id]=809    issue[start_date]=2019-07-27
    ...    issue[custom_field_values][1]=致命的    issue[custom_field_values][2]=robot    issue[custom_field_values][4]=代码问题    issue[custom_field_values][14]=0    issue[custom_field_values][15]=0    attachments[dummy][file]=(binary)
    ...    commit=创建
    ${resp}    Post Request    redmine    /redmine/projects/automatic/issues    params=${params}

modify issue
    ${resp}    get Request    redmine    /redmine/issues/16850
    ${token}    get match    ${tokenReg}    ${resp.text}
    ${lock_version}    get match    ${lock_versionReg}    ${resp.text}
    ${last_journal_id}    Run Keyword If    '${lock_version}'!='0'    get match    ${last_journal_idReg}    ${resp.text}
    ...    ELSE    set variable    ${null}
    ${params}    Create Dictionary    utf8=    _method=patch    authenticity_token=${token}    issue[is_private]=0    issue[project_id]=201 issue[tracker_id]=4
    ...    issue[subject]=modify${lock_version}    issue[status_id]=7    was_default_status=7    issue[priority_id]=13    issue[assigned_to_id]=154    issue[fixed_version_id]=809
    ...    issue[start_date]=2019-07-27    issue[custom_field_values][1]=致命的    issue[custom_field_values][2]=robot    issue[custom_field_values][4]=代码问题    issue[custom_field_values][14]=0    issue[custom_field_values][15]=0
    ...    attachments[1][filename]=xxx.png    issue[lock_version]=${lock_version}    last_journal_id=${last_journal_id}    commit=创建
    ${file_data}    evaluate    open('./pic.png','rb')
    ${files}    Create Dictionary    attachments[1][file]=${file_data}
    ${resp}    Post Request    redmine    /redmine/issues/16850    data=${params}    files=${files}

delete issue
    ${token}    get token
    ${params}    Create Dictionary    _method=delete    authenticity_token=${token}
    ${resp}    Post Request    redmine    /redmine/issues/16845    data=${params}

*** Keywords ***
setup redmine
    ${headers}    Create Dictionary    User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36    Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3    Accept-Language=zh-CN,zh-TW;q=0.9,zh;q=0.8,ja;q=0.7
    Create Session    redmine    http://xxx.redmine.com    headers=${headers}
    login redmine    ${user}    ${pw}

login redmine
    [Arguments]    ${user}    ${pw}
    ${token}    get token
    ${params}    Create Dictionary    utf8=    authenticity_token=${token}    username=${user}    password=${pw}    login=登录 »
    ${resp}    Post Request    redmine    /redmine/login    params=${params}
    Should Contain    ${resp.text}    ${user}

get token
    ${resp}    get Request    redmine    /
    ${token}    get match    ${tokenReg}    ${resp.text}
    [Return]    ${token}

get match
    [Arguments]    ${reg}    ${text}
    ${match}    Get Regexp Matches    ${text}    ${reg}    1
    [Return]    ${match}[0]

总结

Robot Framework是关键字驱动的自动化测试框架,上手真的很方便,不,是超方便!基本上找个入门教程再结合官方文档强烈推荐),几天时间就能写出基本功能的测试代码,不过一些细节地方还是需要深入理解一下的。对于Setup、Teardown和Keywords的灵活运用,可以大大提高代码效率。要想写出高质量的自动化代码,我觉得更重要的是测试思想,而不是用哪种工具。工具和工具的区别,其实是在效率上。

Comments