目标分析
既然是练习,那就从最基本的开始,实现登(登录)增删改功能。用例流程如下:
- 登录
- 新建issue
- 修改issue(增加图片)
- 删除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
...
...
用到的关键字及对应包
具体实现
登录
首先用浏览器工具抓包,查看登录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_version
和last_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