API测试指南

自动化测试从分类上来说,可以把它分为客户端自动化测试和服务端自动化测试,或者可以更加具体的说就是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个阶段。

在这里插入图片描述

这里有我整理的一些资料,如果你不想再体验一次自学时找不到资料,没人解答问题,坚持几天便放弃的感受的话,可以加我们的软件测试交流群 313782132 ,里面有各种软件测试资料和技术交流。

猜你喜欢

转载自blog.csdn.net/weixin_50271247/article/details/109287920