自动化测试从分类上来说,可以把它分为客户端自动化测试和服务端自动化测试,或者可以更加具体的说就是API的自动化测试,API的测试是软件测试的一种测试模式,它包含了两个维度,在狭义的角度上指的是对应用程序接口的功能进行测试,在广义的维度上是指集成测试中,通过调用API测试整体的功能来完成度,可靠性,安全性和性能。相比较客户端自动化测试,API测试是可以有效的提升测试的效率,以及满足在DevOps的理念下的持续交付的能力。另外一个点,目前出去找工作不管是那个级别的测试工程师,都要求会API的测试,只不过不同层级对服务端的测试能力在深度和广度上有区别,但是有一点必须得承认,API的测试技术是每一位测试工程师都要求必须掌握的测试技能。
要系统全面的掌握API的测试能力,可以主要从这么几个维度来看它的技术栈分布和学习路线,第一部分是协议原理;第二部分是API测试的维度,以及如何来测试API,API不是单纯的拿个工具去请求一下,然后返回响应数据就认为已经完成了这样的一个测试过程;第三部分是API测试用例的编写和实战,最后就是它的框架设计和稳定性容量规划的测试,在这里我主要介绍第一至第三部分,后续会逐步的写文章介绍稳定性以及容量测试需要考虑的点。
针对第一部分协议原理,这也是很多人比较模糊而又觉得没必要去学习的知识体系之一,但是在API测试的知识体系中,这部分的知识体系是最核心也是最基础,必须要求掌握的知识。HTTP是基于应用层的协议,它不需要关注底层网络传输层协议的事情,但是这个过程比如,打开浏览器,输入https://item.jd.com/12516591.html,按下回车键,跳转到书籍《Python自动化测试实战》书籍的详情页。感兴趣的同学也可以购买我的书籍:
这个过程的请求步骤是需要掌握的,这就涉及到一个完整的HTTP的请求流程,以及keep-alive的概念以及HTTP版本发展的历史过程。当然还有一个更加具体的例子,比如用户访问你测试的一个系统,用户操作有时候是正常的,有时候提示未知的错误信息,或者说根本没提示任何的错误提示信息,用户无法操作,API的接口都返回是401,那么这个时候到底是什么原因导致的了? HTTP是一个无状态的协议,但是在互联网的产品形态中,一个用户登录系统成功后,需要记录下谁登录了这个系统,以及这个用户它其他的数据信息,这就需要很清楚的掌握COOKIE的请求流程,当然这中间有这么几点是需要清楚的知道的,分别是COOKIE的请求流程,SESSION的请求流程,TOKEN的请求流程,以及HTTPS的请求流程,当然了还有我们经常面试被问到的请求方法,如GET,METHOD请求方法,案例刚才案例说的401错误信息,这些都是基于协议原理的框架下来进行展开的。抛开概念性的东西来说,就拿一个接口测试来说,发送一个接口信息,获取用户主页的信息,有时候可以,有时候又不可以,那么是什么导致了又不可以了?为什么请求它被重定向到登录的页面,应该如何来定位和解决该问题了,如果请求如上说的请求流程,其实就自己能够很清楚的知道是什么原因导致了这样的结果。
继续来看这二部分,API的测试维度,给了一个接口要求对它测试,并且输出测试结果以及判断是否可以上线,那么测试什么?每个测试点都是基于业务形态来展开,真地方我们就抛开业务这部分,如下的接口,我们如何来测试它,接口的源码如下所示
#!/usr/bin/env python
#!coding:utf-8
from flask import Flask,jsonify
from flask_restful import Api,Resource,reqparse
app=Flask(__name__)
api=Api(app)
class LoginView(Resource):
def get(self):
return {'status':0,'msg':'ok','data':'this is a login page'}
def post(self):
parser=reqparse.RequestParser()
parser.add_argument('username', type=str, required=True, help='用户名不能为空')
parser.add_argument('password',type=str,required=True,help='账户密码不能为空')
parser.add_argument('age',type=int,help='年龄必须为正正数')
parser.add_argument('sex',type=str,help='性别只能是男或者女',choices=['女','男'])
args=parser.parse_args()
return jsonify(args)
api.add_resource(LoginView,'/login',endpoint='login')
if __name__ == '__main__':
app.run(debug=True)
基于如上的接口,或者说针对API的测试分类,可以分为单接口的测试,以及针对业务场景的API的测试,我们先来说针对单个API的测试维度,主要可以总结为如下几点:
1、验证必填参数是否为空
2、验证参数的数据类型是否做了校验
3、验证参数的字段⻓度是否做了校验
4、接口的安全性校验和性能校验
对单个API的测试,如果测试的API涉及到支付以及与金钱有关系的接口,都需要考虑API的安全测试,可以从下面几个维度来思考,分别是:
1、是否增加了反爬虫的机制
2、是否增加了请求次数的限制
3、是否增加了对应的请求头信息
3、是否增加了坚权的认证信息(基本认证,常规认证,自定义认证)
4、是否对请求进行了加密
5、是否在被请求的服务端增加了IP的限制(白名单设置和IP的限制请求)
API的性能测试主要是基于服务的测试,可以使用常规的测试工具如JMeter测试工具来进行这部分的测试。
单个接口测试是必要的,但是无法保障到全链路的产品质量保障,所以需要基于产品全链路的质量保障,也就是业务场景的测试,简单的说就是通过API的测试技术,模拟人的操作行为,实现产品业务场景的覆盖,这种覆盖包含了产品正常的业务逻辑以及异常的程序逻辑判断。在基于业务场景的测试中,需要考虑的是参数上下关联的解决方案和思路,如有一个图书管理系统,可以增加书籍,查看增加书籍的信息,修改书籍的信息以及删除数据的信息,那么在链路的测试场景设计中,需要考虑的是添加书籍信息成功后,需要拿到书籍的ID,这样在后面的业务测试中才能够对添加的书籍信息进行信息的查询,信息的修改和信息的删除。这地方就会涉及使用到函数的返回值,把添加书籍成功后书籍ID通过函数返回值返回后,在下个请求中调用这个变量。如编写一个函数返回值的代码具体如下:
#!/usr/bin/env python
#coding:utf-8
def login(username,password):
if username=='wuya' and password=='admin':
return 'dfhj378dfghjw6dfh'
else:
return False
def profile(token):
if token=='dfhj378dfghjw6dfh':
print('欢迎您购买无涯《Python自动化测试实战》视频课程')
else:
return '请先登录系统,谢谢!'
if __name__ == '__main__':
profile(login('wuya','admin'))
下来说说第三部分,也就是API测试用例的编写方法,在一个完整的API测试用例编写中,需要考虑到每个测试点的初始化,测试步骤,测试断言以及清理的操作,在常用的单元测试框架中都已经提供了这部分的信息,如在Python的技术栈中常使用的测试框架Pytest和unittest都提供了这部分的思路和知识体系。完整的测试用例编写思路具体为:
被测试的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':"请认证"}),401)
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, 'user2', 'abcxyz'),
]
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 = [auth.login_required]
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 jsonify({'status':1002,'msg': '添加书籍成功','datas':book}, 201)
class Book(Resource):
# decorators = [auth.login_required]
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)
把请求参数的数据都存储在yaml的文件里面,文件的内容具体为:
---
#查看所有书籍信息
"url": "http://localhost:5000/v1/api/books"
"method": "get"
"expect": '"name": "Python接口自动化测试实战"'
---
#创建书籍信息
"url": "http://localhost:5000/v1/api/books"
"method": "post"
dict1:
"author": "无涯"
"done": true
"name": "Python测试实战"
"expect": '"msg": "添加书籍成功", "status": 1002'
---
#查看id为1的书籍信息
"url": "http://localhost:5000/v1/api/book/1"
"method": "get"
"expect": '"author": "wuya"'
---
#修改id为1的书籍信息
"url": "http://localhost:5000/v1/api/book/1"
"method": "put"
dict1:
"author": "无涯"
"done": true
"name": "Python测试实战"
"expect": '"msg": "更新书的信息成功", "status": 1008'
---
#删除id为1的书籍信息
"url": "http://localhost:5000/v1/api/book/1"
"method": "delete"
"expect": '"msg": "删除书籍成功", "status": 1009'
下面使用Pytest测试框架来演示这部分的测试用例编写,实现代码如下:
#!/usr/bin/env python
#!coding:utf-8
import requests
import pytest
import yaml
import json
def readYaml(filename='books.yaml'):
with open('books.yaml','r',encoding='utf-8') as f:
return list(yaml.load_all(f))
@pytest.mark.parametrize('datas',readYaml())
def test_books(datas,headers):
if datas['method']=='get':
r=requests.get(url=datas['url'],headers=headers)
assert datas['expect'] in json.dumps(r.json(),ensure_ascii=False)
elif datas['method']=='post':
r=requests.post(url=datas['url'],json=datas['dict1'],headers=headers)
assert datas['expect'] in json.dumps(r.json(),ensure_ascii=False)
elif datas['method']=='put':
r=requests.put(url=datas['url'],json=datas['dict1'],headers=headers)
assert datas['expect'] in json.dumps(r.json(),ensure_ascii=False)
elif datas['method']=='delete':
r=requests.delete(url=datas['url'],headers=headers)
assert datas['expect'] in json.dumps(r.json(),ensure_ascii=False)
if __name__ == '__main__':
pytest.main(["-v","-s","test_pytest.py"])
执行后,输出的结果信息如下:
服务端程序的请求流程如下:
框架这部分的设计后续我会详细的在公众号里面介绍它的设计思想,以及结合具体的案例来演示它的应用。我从本周以及未来两周,从三节课程的角度,分别以直播的形式来详细的介绍HTTP协议原理,API测试维度,API测试用例编写和实战三个维度来详细的说明API测试的点,这个过程,可以带领0基础的同学进入到入门的阶段以及下一步的学习思路。本周主要分享HTTP的协议和原理,以及COOKIE,SESSION,TOKEN的原理以及基本认证的详细讲解。也欢迎您的参与。想参与的同学,扫描如下二维码,支付一分钱就可以参加哦,参加后进入VIP的学习群,通过三周三个维度的方式,学会从零到一的API测试维度。直播结束后会分享课程课程以及以及直播期间的案例源码。
软件测试是IT相关行业中最容易入门的学科~不需要开发人员烧脑的逻辑思维、不需要运维人员24小时的随时待命,需要的是细心认真的态度和IT相关知识点广度的了解,每个测试人员从入行到成为专业大牛的成长路线可划分为:软件测试、自动化测试、测试开发工程师 3个阶段。
行动吧,在路上总比一直观望的要好,未来的你肯定会感谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入群: 786229024,里面有各种测试开发资料和技术可以一起交流哦。
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取 【保证100%免费】
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。