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

第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数据自动编码。

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 框架处理流程

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

这里写图片描述

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

  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.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()

分析上述代码的含义:

  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 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()

对上述代码进行分析:

  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 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()

对上述代码进行分析:

  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, 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()

对上述代码进行分析:

  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_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的,其实可以用这个工具。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值