自动化测试:behave
在系统开发过程中,我们一般个人参与的开发都是局部的,自己负责的一部分与其它团队成员的成果组合在一起才能实现用户的完整行为。比如常见的电商系统就有几个主要的流程
- 商品搜索
- 购物车
- 订单
- 支付
- 物流
这些模块复杂的足以形成各自的团队,不同团队相互之间依据一定的接口来配合协作。在开发阶段可以基于接口各自独立开发,对于依赖其它模块的接口可以通过接口mock来完成。但mock方式本身是有使用场景的,一旦依赖方接口稳定后,就会调用实际服务来代替mock接口。
场景A: 在上面提到的购物流程都完善的前提下,一个做物流系统的同事需要真实模似真实的用户场景产生物流单,他需要去相应的环境进行商品搜索,加入购物车,登录,下单,付款等众多操作之后才会轮到他的模块登场。想想都是件相当繁琐的事情,所以希望有一种很快捷的方式代替人工去完成这些复杂且花时间的体力劳动。当然你也可以找测试妹子说:那个谁,帮我在某某环境下一个单我测试物流单逻辑。
期望只需要执行一条命令,就能快速得到指定条件的订单。
场景B: 在上面提到的购物流程中,任意修改某个流程都有可能对整体流程构成不同程度的影响,有没有快速简捷的方式给我们确认新的修改是否会影响主流程的变更呢?一般的作法就是需要测试人员去回归主流程,但这个成本比较高而且并一定那么的可靠,我们需要一种成本低又不知疲倦的工具为我们完成上面的工作,即使达不到100%人工测试的效果。
期望在代码提交后,每日构建工具能够在修改之后的版本上执行主流程的测试用例,确保第一时间反馈出问题来,而不需要等到测试人员报BUG或者是线上用户反馈出问题来才被动知道。
场景C:
在做一个稍微大型的功能后,为了确保各个子功能能够相互协作正常,我们一般首先会对子功能做单元测试,然后对大功能做集成测试。在集成测试时,需要快速的定义测试用例并得到预期结果。
集成测试的方式可能有很多种,比如我之前对http api接口的集成测试就依靠谷歌的postman(当然你也可以用junit来搞集成测试,在里面做各种断言)。但这种方式需要人工判断接口是否正确,因为此工具只具备执行能力并不具备判断能力。所以需要一款脱离特定语言的测试用例工具来帮助我们完成,而且非常容易的能够对环境进行扩展,比如定义开发环境,测试环境,预上线环境,生产环境等。
behave简要说明
依赖项
与python配合完成,所以需要在执行测试用例的机器上至少需要安装:
- python
- behave
另外可以为behave创建单独的python环境,可以引入virtualenv。每次运行时通过source xxxenv/bin/activate来进入特定的python环境,source的目的就是替换环境变量。
其它组件
比如我主要测试http api,所以会用到下面一些库:
- requests, 用于做http请求的
- json,用于处理字符串与json之间的各种转换
- operator,操作符,比如a>b之类的函数表达
参考文档
https://pythonhosted.org/behave/index.html
项目背景
针对HTTP API的集成测试的自动化。上面所说的场景A/B/C是我暂时理解的,不同的人在不现的阶段对自动化测试的需求不一样。比如我做为业务系统的开发者,场景C可以帮助在我提交大型功能前做联调测试,系统稳定后,我们去修改一个功能但依赖其它模块数据时,希望快速产生预期数据那么场景A适合我们。当我们比较惶恐的修改某种功能时,场景B的主流程测试能够给我们信心。
项目结构
这里以文章前面说的购物场景为例。
envbehave
是创建的一个python独立环境,可选。
features
behave相关的所有测试用例文件
- dev 存放测试用例的目录,可以按业务定义名称好作区分,比如订单相关的可以叫 order。
- steps,存放配置测试用例文件的执行文件,behave+python
- environment.py,是为了支持多环境而创建的,比如开发,测试,预上线,生产环境可任意切换
- service.py,封装了基础功能,目的就是简化step以及测试用例的代码
代码实现
多环境支持
behave提供了对于环境的控制,我们可以在如下函数中添加自己的逻辑:
- before_step
- before_scenario
- before_feature
- before_tag
- before_all
根据之前所述,集成测试可能涉及到不同小组提供的api,所以可以定义如下数据:
CONFIG = {
'dev': {
'hosts': {
'product': 'http://localhost:1234/api/product',
'order':'http://localhost:1234/api/order',
'cart': 'http://localhost:1234/api/cart',
'pay': 'http://localhost:1234/api/pay',
'user': 'http://localhost:1234/api/user',
'logistics': 'http://localhost:1234/api/logistics',
}
},
'test': {
'hosts': {
'product': 'http://test.jim.com/api/product',
'order':'http://test.jim.com/api/order',
'cart': 'http://test.jim.com/api/cart',
'pay': 'http://test.jim.com/api/pay',
'user': 'http://test.jim.com/api/user',
'logistics': 'http://test.jim.com/api/logistics',
}
},
}
然后在before_all中进行数据初始化,环境参数可以通过命令行的-D参数来控制,比如: -D env=dev
env = 'dev'
def before_all(context):
global env
if (context.config.userdata.get('env')):
env = context.config.userdata['env']
for k, v in CONFIG[env]['hosts'].items():
hosts[k] = v
基础功能封装
封装通用的功能,便于使用测试用例简单方便,容易管理。创建一个service.py,主体结构如下:
#coding=utf-8
import requests, json
hosts = {}
class BddService(object):
def __init__(self, api, data={}, headers={}):
# 数据初始化
def __before__(self, context, data, url):
# 处理数据,比如从测试用例中取参数,存放到context的requestData中,供后续的http请求使用
def __after__(self, context, r, url):
# 从http request中获取数据,存放到context的responseData中,供后续的断言使用
def get(self, context, url, data={}):
# 完成 http 调用
- 测试用例参数指定
我们需要在用例下面直观灵活的指定参数,可以通过behave提供的文本功能实现,它可以读一段多行文本到context.text变量中,然后我们对去做处理。
When 搜索商品
"""
{"name":"product1"}
"""
在before函数中完成值的填充,将最终的请求参数存放在context的requestData变量中。
def __before__(self, context, data, url):
if context.text:
print (context.text)
o = json.loads(context.text)
print (o)
for k in o:
data[k] = o[k]
context.requestData = data
- HTTP请求结构处理
在after函数中完成取值,将HTTP请求的结构存放在context的responseData变量中。对于HTTP请求的结构支持两类数据,一类是json数据,一类是简单值(比如直接返回一个数字或者一个bool值或者是一个字符串)。
def __after__(self, context, r, url):
try:
context.response = r.json()
if context.response.get('value', None):
context.responseData = context.response.pop('value')
try:
if type(context.responseData) == str or type(context.responseData) == unicode:
context.responseData = json.loads(context.responseData)
except:
if not hasattr(context, 'responseData') or context.responseData == None:
except:
context.response = r.text
上面逻辑中的get('value'),是特殊的数据结构(约定HTTP接口都会按一定的固定格式返回),比如我这里的结构,其中的value才是真正的业务数据。
{
"result": true,
"error": null,
"value": [
{
"id": 1,
"name": "product1",
"productImage": null
}
]
}
断言
behave默认情况下进行断言,需要在@then中完成断言,就需要为每个测试用例编写独立的断言函数,可以做统一的封装,主体支持两类操作。
- 判断请求响应是否正常
- 判断请求的值是否符合预期
创建一个assert.py
- 编写两个断言函数
从@then脚本后面读取多行文本,如果为空直接跳过断言。
@then(u'得到响应数据')
def step_impl(context):
if not context.text:
return
try:
expect = json.loads(context.text)
except:
expect = context.text
assertObj(expect, context.responseData)
@then(u'得到响应')
def step_impl(context):
if not context.text:
return
expect = json.loads(context.text)
assertObj(expect, context.response)
- 编写断言函数
需要判断比较值的类型,因为只支持对基本类型的数据做断言,如果是列表就需要迭代到成员对象,至于迭代到基本数据类型(比如字符串,数字),然后利用operator库做处理。
def assertObj(expect, actual):
if(type(expect) == list):
for i in range(len(expect)):
assertObj(expect[i], actual[i])
elif type(expect)==bool or type(expect)==str or type(expect)==int:
assertObjKey(None, expect, actual)
else:
for k in expect:
if(type(expect[k]) == dict or type(expect[k]) == list):
if(type(actual[k]) != type(expect[k])):
actual[k] = json.loads(actual[k])
assertObj(expect[k], actual[k])
else:
assertObjKey(k, expect[k], actual[k])
def assertObjKey(k,originExpect,actualValue):
#测试用例的值支持<,<=,<,<=,!=,==
#样例数据:{"premium":"ge$0"}
expectArray = str(originExpect).split("$");
if (len(expectArray) == 2):
action = expectArray[0];
realExpect = expectArray[1]
if action == "ge":
assert operator.ge(actualValue, long(realExpect))
elif action == "gt":
assert operator.gt(actualValue, long(realExpect))
elif action == "le":
assert operator.le(actualValue, long(realExpect))
elif action == "lt":
assert operator.lt(actualValue, long(realExpect))
elif action == "ne":
assert operator.ne(actualValue, realExpect)
elif action == "eq":
assert operator.eq(actualValue, realExpect)
else:
assert originExpect == actualValue
else:
assert originExpect == actualValue
优化
上面代码中判断操作符有众多if,基本上就是一种比较一条if,比较冗余,现更新如下:利用getattr来获取方法,与java中的反射有点像。
method= getattr(operator, action,None)
if(method!=None):
method(actualValue, float(realExpect))
else:
assert operator.eq(actualValue, realExpect)
编写step
可以根据调用的不同业务接口创建不同的step文件,比如如下图所示:
这里贴一个登录的step脚本示例,其余的大同小异。
R = BddService('user')
@given(u'初始化数据')
def given_init(context):
context.userName="jim"
context.password="123456"
@when(u'登录')
def step_impl(context):
R.get(context,"/login",{"userName":context.userName,"password":context.password})
创建实例
python中创建实例时没有关键字new,这与其它语言有比较大的区别,刚开始总是觉得别扭,现在看还是别扭。
完成测试用例
创建一个order.feature
Feature:订单流程测试
Scenario:常规下单流程
Given 初始化数据
When 登录
Then 得到响应
"""
{"result":true}
"""
When 搜索商品
"""
{"name":"product1"}
"""
Then 得到响应
"""
{"result":true}
"""
When 加入购物车
Then 得到响应
"""
{"result":true}
"""
When 提交订单
Then 得到响应数据
"""
1
"""
When 支付订单
Then 得到响应数据
"""
1
"""
When 生成物流单
Then 得到响应数据
"""
1
"""
执行测试用例
只需要在对应的目录执行如下脚本即可愉快的执行测试用例。
behave -D env=test features/dev/order.feature
如果运行正常,会看出如下的输出,黄色的代表执行通过,如果执行失败会打印出错误信息,以及用例执行到哪一步报错。另外说明下,behave在执行正常的情况下会屏蔽通过print输出的日志,貌似可以通过参数调,有兴趣的可以研究研究。