《Web接口开发与自动化测试基于Python语言》--第10章

来源:互联网 发布:vm虚拟机 mac 编辑:程序博客网 时间:2024/05/16 04:47

第10章 接口自动化测试框架

本章将介绍接口自动化测试框架的开发,将框架和库进行整合,通过Requests库发送HTTP接口请求,通过unittest单元测试框架组织和运行测试用例,通过HTMLTestRunner生成HTML格式的测试报告,通过PyMySQL驱动操作MySQL数据库来初始化测试数据。

10.1 接口测试工具的不足

接口测试工具的不足点:

  1. 测试数据不可控制

  2. 无法测试加密接口

  3. 扩展能力不足

Ps:对上述问题,Robot Framework都能满足,但是其脚本的可读性差是它最大弱点,如果需要为它开发系统关键字,还不如直接写Python程序。

10.2 Requests库

Requests使用Apache2 Licensed许可证的HTTP库,它基于urllib3,因此继承了urllib3的所有特性,Requests支持HTTP连接保持和连接池,支持使用Cookie保持会话,支持文件上传,支持自动确定响应内容的编码,支持国际化的URL和POST数据自动编码。

  • 英文文档:http://docs.python-requests.org/en/master/

  • 中文文档:http://cn.python-requests.org/zh_CN/latest/

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_code200>>> 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'] == 200assert 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 requestsimport unittestclass 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 框架处理流程

接口自动化测试框架的流程:

这里写图片描述

接口自动化测试框架的处理过程:

  1. 接口测试框架先向测试数据库中插入测试数据;
  2. 调用被测系统所提供的接口;
  3. 系统接口根据传参向测试数据库中进行查询得到查询结果;
  4. 将查询结果组装成一定格式(eg:JSON格式)的数据,并返回给测试框架;
  5. 通过单元测框架断言接口返回的数据,并生成测试报告。

注意:

测试过程,为了正式数据库的数据不受影响,建议使用独立的测试数据库。

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.1port=3306user=rootpassword=nsfocusdb_name=guest

简单封装数据库操作,创建:/home/csg/pyrequest-master/db_fixture/mysql_db.py

#! /usr/bin python# -*- coding:utf8 -*-from pymysql import connect, cursorsfrom pymysql.err import OperationalErrorimport osimport 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()

分析上述代码的含义:

  1. 首先,读取db_config.ini文件中的MySQL数据库连接配置;
  2. 创建DB类,__init__()方法初始化数据库连接,通过connect()方法连接数据库;
  3. 初始化测试数据,这里用到了清除数据clear()、插入数据insert(),insert()方法对插入的数据做了格式化,可将字典转化为插入SQL语句;
  4. 最后,通过close()方法关闭数据库连接。

创建测试数据,/home/csg/pyrequest-master/db_fixture/test_data.py

#! /usr/bin python# -*- coding:utf-8 -*-import syssys.path.append('../db_fixture')try:    from mysql_db import DBexcept 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()

对上述代码进行分析:

  1. init_data()函数用于读取datas字典中的数据;
  2. 调用DB类中的clear()方法清除表数据;
  3. 循环调用insert()方法插入表数据。

10.3.5 编写接口测试用例

创建接口测试用例,/home/csg/pyrequest-master/interface/add_event_test.py

#! /usr/bin/python# -*- coding:utf-8 -*-import unittestimport requestsimport os, sysparentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))sys.path.insert(0, parentdir)from db_fixture import test_dataclass 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()

对上述代码进行分析:

  1. 在接口测试之前,调用test_data.py文件中的init_data()方法,初始化数据库中的测试数据;
  2. 创建AddEventTest测试类,继承unittest.TestCase类;
  3. 创建测试用例,调用添加发布会接口,并验证接口返回的数据;

注意:

  1. 把JSON格式的结果转化为字典赋值给self.result变量,加self的目的是在tearDown()方法中打印self.result变量,打印的结果可以在测试报告中显示,即将接口返回数据打印出来;
  2. 如果不使用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, syssys.path.append('./interface')sys.path.append('./db_fixture')from HTMLTestRunner import HTMLTestRunnerimport unittestfrom 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()

对上述代码进行分析:

  1. 还是先调用test_data.py文件中的init_data()函数来初始化测试数据;
  2. unittest框架提供的discover()方法查找interface目录下,匹配到文件名*_test.py结尾的测试文件;
  3. now按一定格式生成当前时间;
  4. 将文件名命名为now当前时间_result.html并且保存report目录下;
  5. HTMLTestRunner为unittest单元测试框架的扩展,利用它提供的HTMLTestRunner()类来代替unittest单元测试框架的TextTestRunner()类,运行discover中匹配到的测试用例,生成HTML格式的测试报告;

运行测试脚本:python run_tests.py:

  1. 可能需要安装configparser库,pip install configparser;
  2. 在运行过程中,出现了错误:
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_testTraceback (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 10SyntaxError: 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的,其实可以用这个工具。

阅读全文
0 0
原创粉丝点击