第10章 接口自动化测试框架
本章将介绍接口自动化测试框架的开发,将框架和库进行整合,通过Requests库发送HTTP接口请求,通过unittest单元测试框架组织和运行测试用例,通过HTMLTestRunner生成HTML格式的测试报告,通过PyMySQL驱动操作MySQL数据库来初始化测试数据。
10.1 接口测试工具的不足
接口测试工具的不足点:
测试数据不可控制
无法测试加密接口
扩展能力不足
Ps:对上述问题,Robot Framework都能满足,但是其脚本的可读性差是它最大弱点,如果需要为它开发系统关键字,还不如直接写Python程序。
10.2 Requests库
Requests使用Apache2 Licensed许可证的HTTP库,它基于urllib3,因此继承了urllib3的所有特性,Requests支持HTTP连接保持和连接池,支持使用Cookie保持会话,支持文件上传,支持自动确定响应内容的编码,支持国际化的URL和POST数据自动编码。
10.2.1 安装
安装方法:通过PyPI仓库获取安装
Pypi地址:https://pypi.python.org/pypi/requests
通过Requests官方文档提供的第一个例子来体会下它的用法:
>>> import requests
>>> r = requests.get('https://api.github.com/user',auth=('user','pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf-8'
>>> r.encoding
'utf-8'
>>> r.text
'{"login":"defnngj"}, "id":1000588, "avatar_url":......'
>>> r.json()
{'public_gists':0, "id":1000588, "type":......}
>>>
10.2.2 接口测试
查询发布会接口测试用例:
import requests
#查询发布会接口
url = "http://10.18.214.88:8000/api/get_event_list"
#get方法的第一个参数为调用接口的URL地址,params指定接口的入参,将参数定义为字典
r = requests.get(url, params={'eid':'1'})
#json()方法可以将接口返回的JSON格式的数据转化为字典
result = r.json()
#断言接口返回值
#通过assert语句断言字典中的值,即接口返回的数据
assert result['status'] == 200
assert result['message'] == "success"
assert result['data']['name'] == "崔宇婚礼"
assert result['data']['address'] == "公主岭"
assert result['data']['start_time'] == "2017-07-02 12:00:00"
10.2.3 集成unittest
将接口测试脚本继承到unittest单元测试框架中,利用unittest的功能来运行接口测用例:
import requests
import unittest
class GetEventListTest(unittest.TestCase):
"""查询发布会接口测试"""
def setUp(self):
self.url = "http://10.18.214.88:8000/api/get_event_list/"
def test_get_event_null(self):
"""发布会id为空"""
r = requests.get(self.url, params={'eid':''})
result = r.json()
self.assertEqual(result['status'], 10021)
self.assertEqual(result['message'], "parameter error")
def test_get_event_error(self):
"""发布会id不存在"""
r = requests.get(self.url, params={'eid':'901'})
result = r.json()
self.assertEqual(result['status'], 10022)
self.assertEqual(result['message'], "query result is empty")
def test_get_event_success(self):
"""发布会id为1,查询成功"""
r = requests.get(self.url, params={'eid':'1'})
result = r.json()
self.assertEqual(result['status'], 200)
self.assertEqual(result['message'], "success")
self.assertEqual(result['data']['name'], "")
self.assertEqual(result['data']['name'], "崔宇婚礼")
self.assertEqual(result['data']['address'], "公主岭")
self.assertEqual(result['data']['start_time'], "2017-07-02 12:00:00")
if __name__ == '__main__':
unittest.main()
执行方法,同第6章讲解的。
10.3 接口测试框架开发
一个接口测试框架=unittest完成数据验证+HTMLTestRunner来生成测试报告
10.3.1 框架处理流程
接口自动化测试框架的流程:
接口自动化测试框架的处理过程:
- 接口测试框架先向测试数据库中插入测试数据;
- 调用被测系统所提供的接口;
- 系统接口根据传参向测试数据库中进行查询得到查询结果;
- 将查询结果组装成一定格式(eg:JSON格式)的数据,并返回给测试框架;
- 通过单元测框架断言接口返回的数据,并生成测试报告。
注意:
测试过程,为了正式数据库的数据不受影响,建议使用独立的测试数据库。
10.3.2 框架结构介绍
接口自动化测试框架目录结构:
- pyrequest
| > __pycache__
| > db_fixture
| > interface
| > report
| - __init__.py
| - db_config.ini
| - HTMLTestRunner.py
| - README.md
| - run_tests.py
上述文件和目录的作用:
db_fixture/:初始化接口测试数据
interface/:用于编写接口自动化测试用例
report/:生成接口自动化测试报告
db_config.ini:数据库连接配置文件
HTMLTestRunner.py:unittest的扩展,生成HTML格式的测试报告
run_test.py:执行所有接口测试用例的主程序
README.md:说明文档
GitHub项目地址:https://github.com/defnngj/pyrequest
10.3.3 修改数据库配置
编辑配置文件,创建一个测试数据库:/home/csg/guest/guest/settings.py
......
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1',
'PORT': '3306',
'NAME': 'guest_test',
'USER': 'root',
'PASSWORD': '',
#'OPTIONS': {
# 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
#},
}
}
书上说,修改上述配置后,要执行命令:python manage.py migrate
,但是我执行的时候报错了,出现了如下错误:
django.db.utils.InternalError: (1049, u"Unknown database 'guest_test'")
额,原来是要先创建测试数据库,然后才能执行该命令,创建数据库的命令:
create database guest_test character set utf8;
10.3.4 数据库操作封装
创建数据库配置文件:/home/csg/pyrequest-master/db_config.ini
[mysqlconf]
host=127.0.0.1
port=3306
user=root
password=nsfocus
db_name=guest
简单封装数据库操作,创建:/home/csg/pyrequest-master/db_fixture/mysql_db.py
#! /usr/bin python
# -*- coding:utf8 -*-
from pymysql import connect, cursors
from pymysql.err import OperationalError
import os
import configparser as cparser
# ======== 读取db_config.ini文件设置 ===========
base_dir = str(os.path.dirname(os.path.dirname(__file__)))
base_dir = base_dir.replace('\\', '/')
file_path = base_dir + "/db_config.ini"
cf = cparser.ConfigParser()
cf.read(file_path)
host = cf.get("mysqlconf", "host")
port = cf.get("mysqlconf", "port")
db = cf.get("mysqlconf", "db_name")
user = cf.get("mysqlconf", "user")
password = cf.get("mysqlconf", "password")
# ======== 封装MySql基本操作 ===================
class DB:
def __init__(self):
try:
# 连接数据库
self.conn = connect(host=host,
user=user,
password=password,
db=db,
charset='utf8mb4',
cursorclass=cursors.DictCursor)
except OperationalError as e:
print("Mysql Error %d: %s" % (e.args[0], e.args[1]))
# 清除表数据
def clear(self, table_name):
# real_sql = "truncate table " + table_name + ";"
real_sql = "delete from " + table_name + ";"
with self.conn.cursor() as cursor:
cursor.execute("SET FOREIGN_KEY_CHECKS=0;")
cursor.execute(real_sql)
self.conn.commit()
# 插入表数据
def insert(self, table_name, table_data):
for key in table_data:
table_data[key] = "'"+str(table_data[key])+"'"
key = ','.join(table_data.keys())
value = ','.join(table_data.values())
real_sql = "INSERT INTO " + table_name + " (" + key + ") VALUES (" + value + ")"
#print(real_sql)
with self.conn.cursor() as cursor:
cursor.execute(real_sql)
self.conn.commit()
# 关闭数据库连接
def close(self):
self.conn.close()
if __name__ == '__main__':
db = DB()
table_name = "sign_event"
data = {'id':12,'name':'红米','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2016-08-20 00:25:42'}
db.clear(table_name)
db.insert(table_name, data)
db.close()
分析上述代码的含义:
- 首先,读取db_config.ini文件中的MySQL数据库连接配置;
- 创建DB类,__init__()方法初始化数据库连接,通过connect()方法连接数据库;
- 初始化测试数据,这里用到了清除数据clear()、插入数据insert(),insert()方法对插入的数据做了格式化,可将字典转化为插入SQL语句;
- 最后,通过close()方法关闭数据库连接。
创建测试数据,/home/csg/pyrequest-master/db_fixture/test_data.py
#! /usr/bin python
# -*- coding:utf-8 -*-
import sys
sys.path.append('../db_fixture')
try:
from mysql_db import DB
except ImportError:
from .mysql_db import DB
# 创建测试数据
datas = {
# 发布会数据
'sign_event':[
{'id':1,'name':'红米Pro发布会','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'},
{'id':2,'name':'可参加人数为0','`limit`':0,'status':1,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'},
{'id':3,'name':'当前状态为0关闭','`limit`':2000,'status':0,'address':'北京会展中心','start_time':'2017-08-20 14:00:00'},
{'id':4,'name':'发布会已结束','`limit`':2000,'status':1,'address':'北京会展中心','start_time':'2001-08-20 14:00:00'},
{'id':5,'name':'小米5发布会','`limit`':2000,'status':1,'address':'北京国家会议中心','start_time':'2017-08-20 14:00:00'},
],
# 嘉宾表数据
'sign_guest':[
{'id':1,'realname':'alen','phone':13511001100,'email':'alen@mail.com','sign':0,'event_id':1},
{'id':2,'realname':'has sign','phone':13511001101,'email':'sign@mail.com','sign':1,'event_id':1},
{'id':3,'realname':'tom','phone':13511001102,'email':'tom@mail.com','sign':0,'event_id':5},
],
}
# 将测试数据插入表
def init_data():
db = DB()
for table, data in datas.items():
db.clear(table)
for d in data:
db.insert(table, d)
db.close()
if __name__ == '__main__':
init_data()
对上述代码进行分析:
- init_data()函数用于读取datas字典中的数据;
- 调用DB类中的clear()方法清除表数据;
- 循环调用insert()方法插入表数据。
10.3.5 编写接口测试用例
创建接口测试用例,/home/csg/pyrequest-master/interface/add_event_test.py
#! /usr/bin/python
# -*- coding:utf-8 -*-
import unittest
import requests
import os, sys
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parentdir)
from db_fixture import test_data
class AddEventTest(unittest.TestCase):
''' 添加发布会 '''
def setUp(self):
self.base_url = "http://10.18.214.88:8000/api/add_event/"
def tearDown(self):
print(self.result)
def test_add_event_all_null(self):
''' 所有参数为空 '''
payload = {'eid':'','':'','limit':'','address':"",'start_time':''}
r = requests.post(self.base_url, data=payload)
self.result = r.json()
self.assertEqual(self.result['status'], 10021)
self.assertEqual(self.result['message'], 'parameter error')
def test_add_event_eid_exist(self):
''' id已经存在 '''
payload = {'eid':1,'name':'一加4发布会','limit':2000,'address':"深圳宝体",'start_time':'2017'}
r = requests.post(self.base_url, data=payload)
self.result = r.json()
self.assertEqual(self.result['status'], 10022)
self.assertEqual(self.result['message'], 'event id already exists')
def test_add_event_name_exist(self):
''' 名称已经存在 '''
payload = {'eid':11,'name':'红米Pro发布会','limit':2000,'address':"深圳宝体",'start_time':'2017'}
r = requests.post(self.base_url, data=payload)
self.result = r.json()
self.assertEqual(self.result['status'], 10023)
self.assertEqual(self.result['message'], 'event name already exists')
def test_add_event_data_type_error(self):
''' 日期格式错误 '''
payload = {'eid':11,'name':'一加4手机发布会','limit':2000,'address':"深圳宝体",'start_time':'2017'}
r = requests.post(self.base_url, data=payload)
self.result = r.json()
self.assertEqual(self.result['status'], 10024)
self.assertIn('start_time format error.', self.result['message'])
def test_add_event_success(self):
''' 添加成功 '''
payload = {'eid':11,'name':'一加4手机发布会','limit':2000,'address':"深圳宝体",'start_time':'2017-05-10 12:00:00'}
r = requests.post(self.base_url, data=payload)
self.result = r.json()
self.assertEqual(self.result['status'], 200)
self.assertEqual(self.result['message'], 'add event success')
if __name__ == '__main__':
test_data.init_data() # 初始化接口测试数据
unittest.main()
对上述代码进行分析:
- 在接口测试之前,调用test_data.py文件中的init_data()方法,初始化数据库中的测试数据;
- 创建AddEventTest测试类,继承unittest.TestCase类;
- 创建测试用例,调用添加发布会接口,并验证接口返回的数据;
注意:
- 把JSON格式的结果转化为字典赋值给self.result变量,加self的目的是在tearDown()方法中打印self.result变量,打印的结果可以在测试报告中显示,即将接口返回数据打印出来;
- 如果不使用self,又想在报告中显示每个接口返回数据,就只能是在每个用例中print出result,相比来说,还是第一种方法比较方便。
10.3.6 集成测试报告
当用例数量较多,就需要分类管理和执行,为解决这个问题,unittest单元测试框架提供了discover()方法,然后再适用HTMLTestRunner生成HTML格式的测试报告。
创建/home/csg/pyrequest-master/interface/run_tests.py文件:
#! /usr/bin/python
# -*- coding:utf-8 -*-
import time, sys
sys.path.append('./interface')
sys.path.append('./db_fixture')
from HTMLTestRunner import HTMLTestRunner
import unittest
from db_fixture import test_data
# 指定测试用例为当前文件夹下的interface目录
test_dir = './interface'
discover = unittest.defaultTestLoader.discover(test_dir, pattern='*_test.py')
if __name__ == "__main__":
test_data.init_data() # 初始化接口测试数据
now = time.strftime("%Y-%m-%d %H_%M_%S")
filename = './report/' + now + '_result.html'
fp = open(filename, 'wb')
runner = HTMLTestRunner(stream=fp, title='Guest Manage System Interface Test Report', description='Implementation Example with:')
runner.run(discover)
fp.close()
对上述代码进行分析:
- 还是先调用test_data.py文件中的init_data()函数来初始化测试数据;
- unittest框架提供的discover()方法查找interface目录下,匹配到文件名*_test.py结尾的测试文件;
- now按一定格式生成当前时间;
- 将文件名命名为now当前时间_result.html并且保存report目录下;
- HTMLTestRunner为unittest单元测试框架的扩展,利用它提供的HTMLTestRunner()类来代替unittest单元测试框架的TextTestRunner()类,运行discover中匹配到的测试用例,生成HTML格式的测试报告;
运行测试脚本:python run_tests.py:
- 可能需要安装configparser库,pip install configparser;
- 在运行过程中,出现了错误:
Traceback (most recent call last):
File "run_tests.py", line 19, in <module>
test_data.init_data() # 初始化接口测试数据
File "/home/csg/pyrequest-master/db_fixture/test_data.py", line 38, in init_data
db.insert(table, d)
File "/home/csg/pyrequest-master/db_fixture/mysql_db.py", line 59, in insert
cursor.execute(real_sql)
File "/usr/local/lib/python2.7/dist-packages/pymysql/cursors.py", line 166, in execute
result = self._query(query)
File "/usr/local/lib/python2.7/dist-packages/pymysql/cursors.py", line 322, in _query
conn.query(q)
File "/usr/local/lib/python2.7/dist-packages/pymysql/connections.py", line 856, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File "/usr/local/lib/python2.7/dist-packages/pymysql/connections.py", line 1057, in _read_query_result
result.read()
File "/usr/local/lib/python2.7/dist-packages/pymysql/connections.py", line 1340, in read
first_packet = self.connection._read_packet()
File "/usr/local/lib/python2.7/dist-packages/pymysql/connections.py", line 1014, in _read_packet
packet.check_error()
File "/usr/local/lib/python2.7/dist-packages/pymysql/connections.py", line 393, in check_error
err.raise_mysql_exception(self._data)
File "/usr/local/lib/python2.7/dist-packages/pymysql/err.py", line 107, in raise_mysql_exception
raise errorclass(errno, errval)
pymysql.err.InternalError: (1364, u"Field 'create_time' doesn't have a default value")
应该是数据表里create_time字段需要一个默认值,于是修改test_data.py文件,给每个测试数据都增加create_time默认值为当前时间,再次运行就没有这个错误了;
3. 但是继续执行又出现了错误:
Traceback (most recent call last):
File "run_tests.py", line 27, in <module>
runner.run(discover)
File "/home/csg/pyrequest-master/HTMLTestRunner.py", line 632, in run
self.generateReport(test, result)
File "/home/csg/pyrequest-master/HTMLTestRunner.py", line 679, in generateReport
report = self._generate_report(result)
File "/home/csg/pyrequest-master/HTMLTestRunner.py", line 743, in _generate_report
self._generate_report_test(rows, cid, tid, n, t, o, e)
File "/home/csg/pyrequest-master/HTMLTestRunner.py", line 789, in _generate_report_test
status = self.STATUS[n],
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 103: ordinal not in range(128)
问了下度娘,应该是读取文件的时候使用的是ASCII编码,而不是utf-8,自己多余,在每个测试用例的开头都增加了-*- coding:utf-8 -*-,画蛇添足了,去掉后再次运行就没这个错误了;
4. 但是运行结果全部是failed的,查看具体的error信息:
ft5.1: ImportError: Failed to import test module: get_guest_list_test
Traceback (most recent call last):
File "/usr/lib/python2.7/unittest/loader.py", line 254, in _find_tests
module = self._get_module_from_name(name)
File "/usr/lib/python2.7/unittest/loader.py", line 232, in _get_module_from_name
__import__(name)
File "/home/csg/pyrequest-master/interface/get_guest_list_test.py", line 10
SyntaxError: Non-ASCII character '\xe8' in file /home/csg/pyrequest-master/interface/get_guest_list_test.py on line 10, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
原来还是要在每个测试数据前加上utf-8编码的声明,但是每个用例的注释信息不能是中文,奇怪了,没深入研究,反正都修改为英文后,再次运行成功了。
完整的自动化测试报告如下图所示:
总结
其实,虫师是自己完全的封装了一个类似Robot的工具,这个好处是抛开了框架的束缚,可以自由的编写测试用例的内容,只要是python的代码,就可以采用这个工具,自由发挥测试内容,并且结果的展示也比较丰富和友好,如果不打算研究Robot的,其实可以用这个工具。