The pytest
framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries. 这段话很好地阐述了Pytest的设计思想与强大的特性。之前详细地阐述了Pytest测试框架搜索规则、Pytest测试框架执行方式与Pytest测试框架参数化,本文章主要详细地阐述下Pytest测试框架中Fixture特性。
Fixture测试固件特性
在Pytest测试框架中Fixture最核心的特点测试固件的特性(所谓测试固件就就是执行测试用例中初始化与清理的部分),Fixture函数测试固件主要是通过yield来进行体现的。在实际的测试实战中,这样的测试场景其实经常出现,比如查询某条数据,那么它的前置动作是添加数据,后置动作是清理数据,而前置动作与后置动作部分,就是测试固件最直接也是最典型的体现。见如下的测试代码。
#! /usr/bin/env python
# -*- coding:utf-8 -*-
# author:无涯
import pytest
@pytest.fixture()
def init():
print('初始化')
yield
print('清理')
在如上代码中定义了Fixture的函数init,这部分详细的解读具体就是:在Fixture函数init中,yield部分主要为测试步骤与测试验证部分,也就是TestCase真正被执行的逻辑部分,yield前面的代码主要是为了执行yield部分初始化的代码,yield后面的代码主要是yield代码执行后的清理操作,持续完善下代码,增加测试函数,完善后的代码如下。
#! /usr/bin/env python
# -*- coding:utf-8 -*-
# author:无涯
import pytest
@pytest.fixture()
def init():
print('初始化')
yield
print('清理')
def test_case_001(init):
print('测试步骤与测试验证')
在测试函数test_case_001中形式参数增加了Fixture函数init,其实本质上它是Fixture函数的对象,这样执行测试函数test_case_001后,如上截图中可以清晰的看到第一步执行的是初始化,第二部分执行的也就是测试步骤与测试验证部分,最后是清理的部分。理解了Fixture函数测试固件的特性,下面结合一个书籍管理的微服务详细的演示下它在API自动化测试中的案例应用实战,书籍微服务案例代码如下:
from flask import Flask,make_response,jsonify,abort,request
from flask_restful import Api,Resource
from flask_httpauth import HTTPBasicAuth
from flask import Flask
from flask_jwt import JWT, jwt_required, current_identity
from werkzeug.security import safe_str_cmp
app=Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'super-secret'
api=Api(app=app)
auth=HTTPBasicAuth()
@auth.get_password
def get_password(name):
if name=='admin':
return 'admin'
@auth.error_handler
def authoorized():
return make_response(jsonify({'msg':"请认证"}),403)
books=[
{'id':1,'author':'wuya','name':'Python接口自动化测试实战','done':True},
{'id':2,'author':'无涯','name':'Selenium3自动化测试实战','done':False}
]
class User(object):
def __init__(self, id, username, password):
self.id = id
self.username = username
self.password = password
def __str__(self):
return "User(id='%s')" % self.id
users = [
User(1, 'wuya', 'asd888'),
User(2, 'stage', 'asd888'),
User(3,'line','asd888')
]
username_table = {u.username: u for u in users}
userid_table = {u.id: u for u in users}
def authenticate(username, password):
user = username_table.get(username, None)
if user and safe_str_cmp(user.password.encode('utf-8'), password.encode('utf-8')):
return user
def identity(payload):
user_id = payload['identity']
return userid_table.get(user_id, None)
jwt = JWT(app, authenticate, identity)
class Books(Resource):
decorators=[jwt_required()]
def get(self):
return jsonify({'status':0,'msg':'ok','datas':books})
def post(self):
if not request.json:
return jsonify({'status':1001,'msg':'请求参数不是JSON的数据,请检查,谢谢!'})
else:
book = {
'id': books[-1]['id'] + 1,
'author': request.json.get('author'),
'name': request.json.get('name'),
'done': True
}
books.append(book)
return {'status':1002,'msg': '添加书籍成功','datas':book}
# return jsonify({'status':1002,'msg': '添加书籍成功','datas':book}, 201)
class Book(Resource):
decorators = [jwt_required()]
def get(self,book_id):
book = list(filter(lambda t: t['id'] == book_id, books))
if len(book) == 0:
return jsonify({'status': 1003, 'msg': '很抱歉,您查询的书的信息不存在'})
else:
return jsonify({'status': 0, 'msg': 'ok', 'datas': book})
def put(self,book_id):
book = list(filter(lambda t: t['id'] == book_id, books))
if len(book) == 0:
return jsonify({'status': 1003, 'msg': '很抱歉,您查询的书的信息不存在'})
elif not request.json:
return jsonify({'status': 1001, 'msg': '请求参数不是JSON的数据,请检查,谢谢!'})
elif 'author' not in request.json:
return jsonify({'status': 1004, 'msg': '请求参数author不能为空'})
elif 'name' not in request.json:
return jsonify({'status': 1005, 'msg': '请求参数name不能为空'})
elif 'done' not in request.json:
return jsonify({'status': 1006, 'msg': '请求参数done不能为空'})
elif type(request.json['done'])!=bool:
return jsonify({'status': 1007, 'msg': '请求参数done为bool类型'})
else:
book[0]['author'] = request.json.get('author', book[0]['author'])
book[0]['name'] = request.json.get('name', book[0]['name'])
book[0]['done'] = request.json.get('done', book[0]['done'])
return jsonify({'status': 1008, 'msg': '更新书的信息成功', 'datas': book})
def delete(self,book_id):
book = list(filter(lambda t: t['id'] == book_id, books))
if len(book) == 0:
return jsonify({'status': 1003, 'msg': '很抱歉,您查询的书的信息不存在'})
else:
books.remove(book[0])
return jsonify({'status': 1009, 'msg': '删除书籍成功'})
api.add_resource(Books,'/v1/api/books')
api.add_resource(Book,'/v1/api/book/<int:book_id>')
if __name__ == '__main__':
app.run(debug=True,host='0.0.0.0',port='5000')
需要验证的测试场景为查询书籍,初始化是添加书籍,清理是删除书籍,编写的代码具体如下。
#! /usr/bin/env python
# -*- coding:utf-8 -*-
#author:无涯
import pytest
import requests
@pytest.fixture()
def token():
r=requests.post(
url='http://localhost:5000/auth',
jsnotallow={'username':'wuya','password':'asd888'})
return r.json()['access_token']
@pytest.fixture()
def headers(token):
return {'Authorization':'jwt {0}'.format(token)}
def test_all_books(headers):
r=requests.get(
url='http://localhost:5000/v1/api/books',
headers=headers)
print(r.json())
def writeBook(content):
with open('bookID','w') as f:
f.write(content)
def addBook(headers):
r=requests.post(
url='http://localhost:5000/v1/api/books',
jsnotallow={"name":"接⼝测试","author":"⽆涯课堂","done":True},
headers=headers)
print('添加书籍:\n',r.json())
writeBook(str(r.json()['datas']['id']))
def bookID():
with open('bookID') as f:
return f.read()
def delBook(headers):
r=requests.delete(
url='http://localhost:5000/v1/api/book/{0}'.format(bookID()),
headers=headers)
print('删除书籍:\n',r.json())
@pytest.fixture()
def apiInit(headers):
addBook(headers)
yield
delBook(headers)
def test_query_book(apiInit,headers):
r=requests.get(
url='http://127.0.0.1:5000/v1/api/book/{0}'.format(bookID()),
headers=headers)
print('获取添加的书籍信息:\n',r.json())
assert r.json()['datas'][0]['id']==int(bookID())
在如上代码中定义的Fixture函数名称为apiInit,初始化代码为添加书籍的方法addBook,清理代码是删除书籍delBook方法,这样在每次执行测试函数test_query_book的时候第一步是添加书籍,第二步骤是查询书籍并且验证查询的结果信息,最后一步是清理添加的数据,这样的目的是为了保持编写的测试用例的独立性。其实本质上而言,只要深刻理解了测试固件的特性,再结合上面的案例,就很好理解Fixture函数测试固件的特性。
Fixture返回值特性
Fixture函数另外一个特性是返回值的特性,这个特性可以很好的应用在API测试中关于授权认证的部分。在API测试中首先需要获取到TOKEN,然后再下个请求中带上登录成功后返回的TOKEN,那么结合Fixture返回值的特性,可以很轻松的来解决这部分。详细的实战代码见如下。
#! /usr/bin/env python
# -*- coding:utf-8 -*-
# author:无涯
import pytest
import requests
@pytest.fixture()
def login():
r=requests.post(
url='http://0.0.0.0:8000/login/auth/',
jsnotallow={"username":"13484545195","password":"asd888"},
headers={'Content-Type':"application/json"})
return r.json()['token']
def test_index(login):
r=requests.get(
url='http://0.0.0.0:8000/interface/index',
headers={'Authorization':'JWT {login}'.format(login=login)})
assert r.status_code==200
在测试函数test_index中形式参数login其实就是Fixture函数login的对象,那么Fixture函数返回值的内容其实就是该函数对象的值,见debug模式下显示的信息。
其实要深刻的理解这部分,首先还是需要理解在Python中一切皆对象这个设计思想,这个设计思想中的对象它可以是一个变量,或者是一个函数,或者是一个类。在这个案例中,对象login它就是一个函数,是Fixture函数login的对象。
Fixture的重命名
在Pytest测试框架中也可以对Fixture函数进行重命名,˙这样在调用的时候直接使用重命名后的名称。Fixture函数中重命名的关键字是name,实现的案例代码如下所示。
#! /usr/bin/env python
# -*- coding:utf-8 -*-
# author:无涯
import pytest
import requests
@pytest.fixture(name='token')
def login():
r=requests.post(
url='http://0.0.0.0:8000/login/auth/',
jsnotallow={"username":"13484545195","password":"asd888"},
headers={'Content-Type':"application/json"})
return r.json()['token']
def test_index(token):
r=requests.get(
url='http://0.0.0.0:8000/interface/index',
headers={'Authorization':'JWT {login}'.format(login=token)})
assert r.status_code==200
if __name__ == '__main__':
pytest.main(["-s","-v","test_fixture.py"])
指定Fixture作用范围
Fixture中包含一个scope的关键字可以指定Fixture函数的作用范围,主要用于控制Fixture函数执行前置与执行后置的频率,作用范围分别是function、class、module、session,如果编写的Fixture函数没指定scope默认是function,下面针对不同作用范围详细的阐述下,具体如下。
- function:函数级别的Fixture在每个测试函数只运行一次。
- class:类级别的scope不管测试类中有多少个测试方法,都可以共享这个Fixture并且每个测试类只执行一次。
- module:每个模块只需要执行一次,不管这个模块里面有多少个测试类与测试函数。
- session:会话级别的Fixture每次会话只需要执行一次,在一个Pytest会话中所有的测试函数(测试方法)都共享这个。
那么涉及到一个问题,在企业级里面使用的时候按那个scope范围来使用了,我的建议是按默认的作用范围来使用就可以了。
Fixture的参数化
使用Fixture也可以实现参数化,在Fixture中使用param关键字获取到需要参数化的数据,Fixture参数化案例代码如下。
#! /usr/bin/env python
# -*- coding:utf-8 -*-
# author:无涯
import pytest
def add(a,b):
return a+b
def data():
return [
{"x":1,"y":4,"result":5},
{"x":"wuya","y":"Share","result":"wuyaShare"},
{"x":1.0,"y":4.0,"result":5.0},
{"x":1,"y":4.0,"result":5.0},
{"x":[1,2,3],"y":[4,5,6],"result":[1,2,3,4,5,6]}
]
@pytest.fixture(params=data())
def getData(request):
return request.param
def test_add_fixture_params(getData):
assert add(a=getData['x'],b=getData['y'])==getData['result']
如上所示可以看到在getData函数中通过param关键字获取到参数化的数据,然后在测试函数中getData可以依次获取里面的数据,如上代码执行后的结果信息如下所示。
在Pytest测试框架中Fixture函数具备很强大的特性,上面分别从传递测试数据、测试固件、重命名、参数化等方面进行了详细的阐述。掌握这些特性能够解决在企业中自动化测试的事宜。感谢您的阅读。