Ⅸ unittest框架实现API自动化测试


 

第 1 章. 小摸底

1.框架整合

1.1)项目分层结构

 # windows文件夹名字不区分大小写,Linux区分大小写

├─common
├─logs
├─reports 
├─testcases
├─test_data
├─settings.py
└─main.py


1.2)excel用例数据读取功能的修改

 

大多数情况简单功能(代码行数较少,逻辑简单)可以直接封装成函数,复杂,多功能进行面向对象封装。

testcase.xlsx

 

 

data_handler.py

from openpyxl import load_workbook


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


if __name__ == '__main__':
res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
print(res)


1.3)日志处理模块的修改与应用

 log_handler.py

import logging
from logging.handlers import TimedRotatingFileHandler


def get_logger(name, filename, encoding='utf-8', fmt=None, when='d', interval=1, backup_count=7, debug=False):
"""

:param name: 日志器的名字
:param filename: 日志文件名(包含路径)
:param encoding: 字符编码
:param fmt: 日志格式
:param when: 日志轮转时间单位
:param interval: 间隔
:param backup_count: 日志文件个数
:param debug: 调试模式
:return:
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 文件日志处理器的等级一般情况一定比控制台要高
# 如果创建日志器的时候,debug=True,是调试模式
if debug:
file_level = logging.DEBUG # 就把文件日志处理器的等级设为DEBUG
console_level = logging.DEBUG # 控制台日志处理器的等级设为DEBUG
# 如果不是调试模式
else:
file_level = logging.WARNING
console_level = logging.INFO

if fmt is None:
fmt = '%(levelname)s %(asctime)s [%(filename)s-->line:%(lineno)d]:%(message)s'

file_handler = TimedRotatingFileHandler(
filename=filename, when=when, interval=interval, backupCount=backup_count, encoding=encoding)
file_handler.setLevel(file_level)

console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

return logger


if __name__ == '__main__':
import settings

log = get_logger(**settings.LOG_CONFIG)
log.info('我是普通信息')
log.warning('我是警告信息')

1.4)项目配置与路径处理

 路径需要动态生成绝对路径

1.全局变量 __file__,它的值永远都是当前模块的绝对路径,其中settings.py文件永远都在根目录下

2.os.path.dirname(__file__)得到的是当前模块所在的目录

3.os.path.abspath(__file__)解决正反斜杠的问题

settings.py

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

 

2.注册接口测试

2.1)接口测试用例编写步骤

1. 处理测试数据
- 读取excel里的用例数据
- 解析请求数据,期望结果数据
2. 发送请求
- 根据请求数据,发送对应的请求
3. 断言
- 断言状态码
- 断言响应数据
- 数据库校验

 

2.2)测试用例类代码编写

__init__.py

# python中的每一个包下都会有一个__init__.py文件
# 可以通过__init__实现单例模式

# 当包被导入的时候,这个__init__.py文件里的代码会被执行,并且只会执行一遍
import unittest
import settings
from common.log_handler import get_logger
logger = get_logger(**settings.LOG_CONFIG)
# logger就是一个单例模式
# __init__的属性会直接挂在common上


test_register.py-注册接口测试用例类编写

# ⭐先导入系统库,再导入第三方库,后导入自己内部的模块

 

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/7/20 21:21
# @Author : 周超
import json
import unittest

import requests
from unittestreport import ddt, list_data

import settings
from common import logger
from common.test_data_handler import get_data_from_excel
from common.make_requests import send_http_request

cases = get_data_from_excel(settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 注册接口 开始测试 =======')

@classmethod
def tearDownClass(cls) -> None:
logger.info('========= 注册接口 测试结束 =======')

@list_data(cases)
def test_register(self, case):
logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 1. 处理测试数据
# 读取excel里的用例数据,将请求数据和期望数据转换为python对象
request_data = json.loads(case['request_data'])
expect_data = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**request_data
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(expect_data, {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], expect_data))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))
# 3.3 数据库校验

 

2.3)发送http请求模块封装

 

功能分析:
- 向不同url发送http请求
- 发送各种方法的http请求
- 发送请求是可以携带各种参数(json, data ,file, params, headers, cookies)

功能单一 封装成一个函数

 

封装思路:
1. 输入参数 url, method, 以及需要携带的各种类型的http请求参数和请求头等
2. 可以使用动态关键字参数解决第一个步参数过的问题
3. 使用requests库请求方法的同名参数便于传递
4. 根据传入的method,发送对应的请求

 

 

make_requests.py

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/7/20 21:46
# @Author : 周超
"""
封装思路:
1. 输入参数 url, method, 以及需要携带的各种类型的http请求参数和请求头等
2. 可以使用动态关键字参数解决第一步参数过多的问题
3. 使用requests库请求方法的同名参数便于传递
4. 根据传入的method,发送对应的请求
"""
import requests


def send_http_request(url, method='get', **kwargs):
"""
发送http请求
:param url:
:param method:
:param kwargs: 接受requests库原生请求方法的关键字参数
:return:
"""
# 为了防止用户传入的方法名的大小写格式问题
# 统一一下大小写格式
method = method.lower()
# 根据方法名发送对应的请求
# kwargs = {'json': {'mobile_phone': '111111'}}
if method == 'get':
res = requests.get(url, **kwargs) # =》requests.get(url, json={'mobile_phone': '111111'})
elif method == 'post':
res = requests.post(url, **kwargs)
elif method == 'put':
res = requests.put(url, **kwargs)
elif method == 'patch':
res = requests.patch(url, **kwargs)
elif method == 'delete':
res = requests.delete(url, **kwargs)
else:
raise ValueError('请输入正确的请求方法!')
return res


if __name__ == '__main__':

send_http_request('', 'post', json={'mobile_phone': '111111'})


main.py

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/7/20 20:06
# @Author : 周超
import unittest

from unittestreport import TestRunner

import settings


if __name__ == '__main__':
# 1. 收集用例
ts = unittest.defaultTestLoader.discover(settings.TESTCASES_DIR)
# 2. 运行并生成测试报告
TestRunner(ts, **settings.REPORT_CONFIG).run()

 

2.4)反射-http请求新封装

使用`getattr`这个函数来进一步的封装

思想: 把不同的请求函数看做requests库的属性

 

 

['ConnectTimeout',
 'ConnectionError',
 'DependencyWarning',
 'FileModeWarning',
 'HTTPError',
 'NullHandler',
 'PreparedRequest',
 'ReadTimeout',
 'Request',
 'RequestException',
 'RequestsDependencyWarning',
 'Response',
 'Session',
 'Timeout',
 'TooManyRedirects',
 'URLRequired',
 '__author__',
 '__author_email__',
 '__build__',
 '__builtins__',
 '__cached__',
 '__cake__',
 '__copyright__',
 '__description__',
 '__doc__',
 '__file__',
 '__license__',
 '__loader__',
 '__name__',
 '__package__',
 '__path__',
 '__spec__',
 '__title__',
 '__url__',
 '__version__',
 '_check_cryptography',
 '_internal_utils',
 'adapters',
 'api',
 'auth',
 'certs',
 'chardet',
 'check_compatibility',
 'codes',
 'compat',
 'cookies',
 'delete',
 'exceptions',
 'get',
 'head',
 'hooks',
 'logging',
 'models',
 'options',
 'packages',
 'patch',
 'post',
 'put',
 'request',
 'session',
 'sessions',
 'ssl',
 'status_codes',
 'structures',
 'urllib3',
 'utils',
 'warnings']

 

make_requests.py

"""

1. 输入参数 url, method, 以及需要携带的各种类型的http请求参数和请求头等
2. 可以使用动态关键字参数解决第一个步参数过的问题
3. 使用requests库请求方法的同名参数便于传递
4. 根据传入的method,发送对应的请求
"""
import requests


def send_http_request(url, method='get', **kwargs):
"""
发送http请求
:param url:
:param method:
:param kwargs: 接受requests库原生请求方法的关键字参数
:return:
"""
# 为了防止用户传入的方法名的大小写格式问题
# 统一一下大小写格式
method = method.lower()
# 根据方法名发送对应的请求
return getattr(requests, method)(url, **kwargs)

# kwargs = {'json': {'mobile_phone': '111111'}}
# if method == 'get':
# res = requests.get(url, **kwargs) # =》requests.get(url, json={'mobile_phone': '111111'})
# elif method == 'post':
# res = requests.post(url, **kwargs)
# elif method == 'put':
# res = requests.put(url, **kwargs)
# elif method == 'patch':
# res = requests.patch(url, **kwargs)
# elif method == 'delete':
# res = requests.delete(url, **kwargs)
# else:
# raise ValueError('请输入正确的请求方法!')
# return res


if __name__ == '__main__':

send_http_request('', 'post', json={'mobile_phone': '111111'})

2.5)配置化补充

url 配置化->

settings.py

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
'register': PROJECT_HOST + '/member/register'
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}


 

 

 

第 2 章. 数据库校验

1.python操作sql数据库

1.1)pymysql,mysqlclient

支持:
python 2.7 and 3.5 +
mysql server 5.5 +

pip install PyMySQL

 

 

1.2)使用步骤

1. 创建连接 # 买路
2. 创建游标 # 租车
3. 执行SQL语句 # 提货单
4. 获取结果 # 卸货
5. 关闭游标 # 还车
6. 关闭连接 # 卖路

 

1.3)查询数据

import pymysql
"""
主机:api.lemonban.com
port:3306
用户:future
密码:123456
"""
# 1. 创建连接 连接数据库 # 买路
conn = pymysql.connect(
host='api.lemonban.com', # 主机
user='future', # 用户名
password='123456', # 密码
db='futureloan', # 数据库
port=3306, # 端口
charset='UTF8mb4' # 字符串编码
cursorclass=pymysql.cursors.DictCursor # ⭐数据库的查询结果就是字典的格式保持
)
# 2. 创建游标 # 租车,租不同的车
cursor = conn.cursor(pymysql.cursors.DictCursor)
# 3. 执行SQL语句 # 提货单
sql = 'select id, mobile_phone from member limit 10'
cursor.execute(sql)
# 4. 获取结果 # 卸货
# 获取一条
one = cursor.fetchone()
print(one)
# 获取多条
three = cursor.fetchmany(3)
print(three)
# 获取所有
other = cursor.fetchall()
print(other)
# 5. 关闭游标 # 还车
cursor.close()
# 6. 关闭连接 # 卖路
conn.close()

⭐新创建游标,不会受之前查询的影响 



1.4)更新数据

pymysql:默认开启事务,查询时也是开启事务。

注意:是要要提交

           SQL出错要回滚

import pymysql
db_config = {
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db':'futureloan', # 数据库
'port': 3306, # 端口
'charset': 'utf8' # 字符串编码

}

conn = pymysql.connect(**db_config)
with conn.cursor() as cursor:
# 构造sql
# 转账
sql1 = 'update member set leave_amount=leave_amount-600 where id=2'
sql2 = 'update member set leave_amount=leave_amount+600 where id=1'
cursor.execute(sql1)
cursor.execute(sql2)
conn.commit()
conn.close()


2.数据库查询功能封装

2.1)功能分析

1. 可以连接不同的sql数据库(高级功能,扩展的思路)
2. 查询一条数, 查询多条数据
3. 可以获取不同格式的数据

 

2.2)封装数据库查询类

封装思路:
1. 数据库查询模块有多个功能,且需要复用,最好封装成类
2. 在构造方法中创建连接(因为连接只要创建一遍,而游标每次新建)
3. 创建对象方法实现各种功能

 

db_handler.py

import pymysql
from pymysql.cursors import DictCursor


class SQLdbHandler:
"""
sql数据库查询类
"""
def __init__(self, db_config):
# 创建连接
# 根据不同的数据库,创建不同的连接
engine = db_config.pop('engine', 'mysql')
if engine.lower() == 'mysql':
self.conn = pymysql.connect(**db_config)

def get_one(self, sql, res_type='t'):
"""
获取一条数据
:param sql:
:param res_type: 返回数据的类型 默认为't表示以元组的形式返回
'd'表示以字典的形式返回
:return: 元组/字典
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchone()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchone()

def get_many(self, sql, size, res_type='t'):
"""
获取多条数据
:param sql:
:param size: 指定的条数
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)

def get_all(self, sql, res_type='t'):
"""
获取所有数据
:param sql:
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchall()

def exist(self, sql):
"""
查询数据是否存在
:param sql:
:return:
"""
if self.get_one(sql):
return True
else:
return False

def __del__(self):
# pythoon是解析型语言,当脚本执行完后,会销毁当前内存里面的所有对象
"""
对象销毁的时候自动被调用
:return:
"""
self.conn.close()


if __name__ == '__main__':
import settings
db = SQLdbHandler(settings.DB_CONFIG)
sql = 'select leave_amount from member where id=100000000000000000'
res = db.get_one(sql, 'd')
print(res)
print(db.exist(sql))

 

测试用例-拼接URL->

增加数据库断言->

testcase.xlsx

 

test_register.py

正例和一些修改关键数据的时候才需要做数据库校验

import json
import unittest

import requests
from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.test_data_handler import get_data_from_excel
from common.make_requests import send_http_request


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 注册接口 开始测试 =======')

@classmethod
def tearDownClass(cls) -> None:
logger.info('========= 注册接口 测试结束 =======')

@list_data(cases)
def test_register(self, case):
logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 1. 处理测试数据
# 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]

# 将请求数据和期望数据转换为python对象
request_data = json.loads(case['request_data'])
expect_data = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**request_data
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(expect_data, {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], expect_data))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))

# 3.3 数据库断言
if case['sql']:
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))

增加数据库配置->

settings.py

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
'register': PROJECT_HOST + '/member/register'
}

# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'futureloan', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True # 可重复读(如果不可重复度,后面读的数据就会不一样,我们需要每查询一次就自动提交一次)
# 如果是修改数据库,就要是Fasle
}

 

common中增加数据库参数->

 

__init__.py

# python中的每一个包下都会有一个__init__.py文件

# 当包被导入的时候,这个__init__.py文件里的代码会被执行,并且只会执行一遍
import unittest
import settings
from common.log_handler import get_logger
from common.db_handler import SQLdbHandler

logger = get_logger(**settings.LOG_CONFIG)
# 数据库查询类又只会被实例化一次,达到复用的效果
db = SQLdbHandler(settings.DB_CONFIG)


 

第 3 章. 测试数据动态生成

1.随即库random

伪随机

有些场景需要复现数据的时候,可以循环中设定一个固定的随机数种子,每次生成一个固定的随机数

默认以当前时间作为随机数种子

1.1)生成[0-1)之间的随机浮点数

 

 

1.2)生成[a,b]之间的随机整数

 

 

 

1.3)生成[a,b)之间的随机整数

 

 

 

1.4)在一个序列中随机选择一个元素

 

 

 

2.封装生成手机号码的函数

功能分析:

  1. 随机生成符合规则的手机号码

    手机号码的不严格规则如下:

    • a. 11位数字

    • b. 第一位数字是1

    • c. 第二位数字3-9,具体项目

  2. 生成的手机号码要没有使用过

    • 查询数据库

data_handler.py

增加生成随机手机号码函数

import random

from openpyxl import load_workbook
from common import db


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


def generate_phone():
"""
随机的生成手机号码
:return:
"""
# 1开头
# 11位
# 第二个数字是3-9

# phone = ['1', str(random.randint(3, 9))]
phone = ['158']
for i in range(8):
phone.append(str(random.randint(0, 9)))
return ''.join(phone) # 替换的时候要用字符串,所有生成手机号码的时候生成字符串


def generate_no_use_phone(sql="select id from member where mobile_phone='{}'"):
"""
生成没有注册的手机号码
:return:
"""
while True:
phone = generate_phone()

if not db.exist(sql.format(phone)):
return phone


if __name__ == '__main__':
# res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
# print(res)
# print(generate_phone())
print(generate_no_use_phone())


testcase.xlsx

测试数据添加手机号的‘生成标志’与‘替换标志’

 

3.faker

生成一些伪造的其他随机数:如地址 、 姓名

 

文档地址Locale zh_CN — Faker 13.3.2 documentation

 

 

 

 

 

 有些没有提供:如automotive()

 

 # 可以看看中国提供的有:

 

 

4.应用入项目

1. 思路

  1. 在用例的数据中添加生成手机号码的标志,设计标志为

    $特殊的变量名$

    例如生成手机号码的标志位:$phone$

  2. 检查用例数据,如果出现标志位则动态生成对应的数据并替换

  3. 检查 url, request_data

  4. 在一条用例中生成的数据可能会在多处使用(替换), 例如注册时生成的手机号码,注册成功以后还需要在sql去校验,需要在生成手机号码时,保存这个号码,然后替换sql中的替换标志。

  5. 设计替换标志为#变量名#, 例如手机号码的替换标志为#phone#

     

2.修改用例数据

`在拼接url之前,添加生成动态数据的代码

`修改将json转化为python对象代码

`修改发送请求的动态关键字参数

`和修改断言中的python对象

 

test_register.py

import json
import unittest

import requests
from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.test_data_handler import get_data_from_excel
from common.test_data_handler import generate_no_use_phone
from common.make_requests import send_http_request


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 注册接口 开始测试 =======')

@classmethod
def tearDownClass(cls) -> None:
logger.info('========= 注册接口 测试结束 =======')

@list_data(cases)
def test_register(self, case):
logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 1. 处理测试数据
# 1.1 生成动态数据并替换
if '$phone$' in case['request_data']:
# 如果request_data中存在生成数据的标志
phone = generate_no_use_phone()
# 立刻替换
case['request_data'] = case['request_data'].replace('$phone$', phone)
if case.get('sql'):
case['sql'] = case['sql'].replace('#phone#', phone)

# 1.2 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]

     # 1.3 原代码
     request_data= json.loads(case['request_data'])
     expect_data = json.loads(case['expect_data']) 
     # 1.3将请求数据和期望数据转换为python对象
        case['request_data'] = json.loads(case['request_data'])
case['expect_data'] = json.loads(case['expect_data'])

    # 2.原发送请求代码
      #  response = send_http_request(
# case['url'],
# case['method'],
# **request_data
# )
        # 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**case['request_data']
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
# 修改断言中的python对象
self.assertEqual(case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], case['expect_data']))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))

# 3.3 数据库断言
if case['sql']:
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))

 

 

第 4 章. 接口依赖

一个接口的测试经常需要依赖另外一个或者多个接口成功请求之后返回的数据。

充值接口 依赖 注册,登录接口

这些依赖,可以放在前置条件中,脚手架代码中去处理

1.类级前置条件处理

当一个接口的前置依赖接口只需要在整个测试开始前请求一遍,这时,就可以在类级前置方法`setUpClass`中去处理即可。

 

如何将前置接口返回的数据传递到后面的单元测试方法中?

1.1)解决方案

1. 全局变量

定义一个全局变量(容器类型,列表,字典,自定义类),在前置方法中将需要传递的数据绑定到这些容器里,然后再到测试方法中,通过全局变量去获取。

 

2. 使用测试用例类本身

思路和第一种方案一样

 

demo.py

"""
如何将setUpClass方法中收到的数据传递到后面的测试方法中
"""
import unittest


# ①定义一个全局变量类
class EnvData:
pass


EnvData1 = {} # ②定义一个全局变量


def do_something():
return 3.14


class SomeTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
# 前置操作
data = do_something()
# 1. 将data绑定到全局变量类的属性上
EnvData.data = data
EnvData1['data'] = data

# 2. ③将data绑定到当前的类(测试用例类SomeTestCase)aaa属性上
cls.aaa = data

def test_something(self):
print('测试执行')
# 1. 从全局变量中获取上面绑定的数据data
print(EnvData.data)
print(EnvData1['data'])

# 2. 从当前用例对象(对象方法的对象是self)获取当前的类self.__class__-》测试用例类(SomeTestCase)-》上面去获取绑定的数据
# 收集用例的时候,会实例化为用例对象,测试测试用例的时候是用例对象去执行的
print(self.__class__.aaa)
# 如果当前用例对象没有同名的对象属性(当前用例对象上没有aaa属性),也可以直接通过对象获取
# 从当前用例对象上取获取属性时,首先找的是对象,对象如果没有aaa属性,就会去找他的类
print(self.aaa)

1.2)充值接口测试

1.2.1 充值接口分析

1. 接口依赖(前置条件)


- 注册用户
- 登录用户


2. 权限验证

1.2.2 封装注册,登录用户的函数

配置里面-增加充值接口和登录接口的地址->

settings.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/7/20 20:06
# @Author : 心蓝
import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
# =================== 项目模块 =========================
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'future', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}

1.2.3 充值接口的用例编写

testcase.xlsx

 

 

 

1.2.4 充值接口测试用例代码编写

test_recharge.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/7/27 21:07
# @Author : zhouchao
import json
import unittest

from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.fixture import register, login
from common.make_requests import send_http_request
from common.test_data_handler import (
generate_no_use_phone,
get_data_from_excel)


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'recharge')


@ddt
class TestRecharge(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 充值接口 开始测试 =======')
# 1. 注册一个用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, '12345678')

# 2. 登录
data = login(mobile_phone, '12345678')

# 3. 绑定数据
# 绑定用户的id,绑定token
# 绑定到类属性中
cls.member_id = data['id']
cls.token = data['token_info']['token']

@classmethod
def tearDownClass(cls) -> None:
logger.info('========= 充值接口 测试结束 =======')

@list_data(cases)
def test_recharge(self, case):
# 1. 处理测试数据
# 1.1 生成动态数据并替换

# 1.2 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]
# 1.3 替换依赖参数
# 设计好的槽位 #token# #member_id#
# ⑤因为如果case['request_data']中没有#token#或#member_id#,
# 而为空值,''.replace('#token#', self.token)不会报错
# 所以下面两行不需要判断
# ⑥当前对象没有token和member_id属性时,可以直接使用self.token,而不用写self.__class__.token
case['request_data'] = case['request_data'].replace('#token#', self.token)
case['request_data'] = case['request_data'].replace('#member_id#', str(self.member_id))
# sql也需要替换
# ③如果数据库那一列没有sql语句,则单元格的值为None,None没有replace这个方法
# 运行会报错:AttributeError:'NoneType' object has no attribute 'replace'
# 所以替换sql里面的槽位需要加if来判断
# ④因为有可能没有sql这一列,就会报错,所以不能用case['sql'],要用字典的get方法case.get('sql')
# 如果没有数据,则返回None
if case.get('sql'):
case['sql'] = case['sql'].replace('#member_id#', str(self.member_id))
# 1.4 将json格式的请求数据和期望数据转换为python对象
# ②用例设计:#token#需要包裹在""里面,而#member_id#和$phone$不需要,是因为电话号码和用户id是数字类型
# 生成整数替换之后,再用json.loads之后,就会变成整数
# 而token是字符串类型,所以要放在双引号里面
# ①要先将依赖参数替换之后,再转换为python对象,因为如果不替换,则不是json格式,loads会报错
case['request_data'] = json.loads(case['request_data'])
case['expect_data'] = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**case['request_data']
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型,可扩展
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], case['expect_data']))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))
# 3.3 数据库断言
if case.get('sql'):
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))

1.2.5 common下脚手架代码编写

fixture.py
"""
一些脚手架代码
"""
import requests

import settings
from common import logger


def register(mobile_phone, pwd, reg_name=None, _type=None):
"""
注册用户
:param mobile_phone:
:param pwd:
:param reg_name:
:param _type:因为type是一个内置函数,所有加个_,才不会覆盖
:return:
"""
# 1. 构造发送注册请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
if reg_name:
data['reg_name'] = reg_name

if _type is not None:
data['type'] = _type

headers = {"X-Lemonban-Media-Type": "lemonban.v1"}
url = settings.INTERFACES['register']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
logger.info('注册用户成功')
return res.json()['data']
else:
raise ValueError(res.json())
except Exception as e:
logger.warning('注册用户失败')
raise e


def login(mobile_phone, pwd):
# 1. 构造发送登录请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
# v2才会返回token
headers = {"X-Lemonban-Media-Type": "lemonban.v2"}
url = settings.INTERFACES['login']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
logger.info('登录用户成功')
return res.json()['data']
else:
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone = generate_no_use_phone()
register(phone, '12345678')
res = login(phone, '12345678')
print(res)

2.方法级前置条件处理

当一个接口的前置依赖接口在每一次测试前都要请求一遍的时候,就可以在方法级前置方法`setUp`中去处理。

2.1)解决方案

1. 全局变量/类属性
2. 对象属性

demo.py

import random

import unittest


def do_something():
return random.randint(0, 9)


class SomeTestCase(unittest.TestCase):

def setUp(self) -> None:
# 执行前置操作
data = do_something()
print(data)
# 绑定到对象属性上
self.data = data

def test_01something(self):
print('执行测试1')
# 在测试方法里面拿到data,对象方法可以用对象属性取值
print('获取前置方法数据: {}'.format(self.data))

def test_02something(self):
print('执行测试2')
print('获取前置方法数据: {}'.format(self.data))

2.2)项目审核接口测试

2.2.1 接口分析

- 类级前置条件
1. 注册普通用户
2. 登录普通用户
3. 创建管理员用户
4. 登录管理员用户


- 方法级前置条件
1. 创建项目

 

2.2.2 封装创建项目的函数

增加项目接口地址-》

settings.py
import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
# =================== 项目模块 =========================
'add': PROJECT_HOST + '/loan/add',
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'future', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}

在脚手架代码里封装创建项目的函数

fixture.py
"""
一些脚手架代码
"""
import requests

import settings
from common import logger


def register(mobile_phone, pwd, reg_name=None, _type=None):
"""
注册用户
:param mobile_phone:
:param pwd:
:param reg_name:
:param _type:
:return:
"""
# 1. 构造发送注册请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
if reg_name:
data['reg_name'] = reg_name
# 因为会传入0进来,所有不能直接if _type
if _type is not None:
data['type'] = _type

headers = {"X-Lemonban-Media-Type": "lemonban.v1"}
url = settings.INTERFACES['register']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('注册用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('注册用户失败')
raise e


def login(mobile_phone, pwd):
# 1. 构造发送登录请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
# v2才会返回token
headers = {"X-Lemonban-Media-Type": "lemonban.v2"}
url = settings.INTERFACES['login']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('登录用户成功')
return res.json()['data']
# 如果上面成功,就不会执行抛出报错信息
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


def add_loan(member_id, token, title='借钱实现财富自由', amount=5000, loan_rate=12.0,
loan_term=3, loan_date_type=1, bidding_days=5):
"""
添加一个项目
:param member_id:
:param token:
:param title:
:param amount:
:param loan_rate:
:param loan_term:
:param loan_date_type:
:param bidding_days:
:return:
"""
# 1. 构造发送添加项目请求的参数
data = {
'member_id': member_id,
'title': title,
'amount': amount,
'loan_rate': loan_rate,
'loan_term': loan_term,
'loan_date_type': loan_date_type,
'bidding_days': bidding_days
}
# 需要鉴权
headers = {"X-Lemonban-Media-Type": "lemonban.v2", 'Authorization': 'Bearer {}'.format(token)}
url = settings.INTERFACES['add']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('创建项目成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('创建项目失败')
raise e


if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone = generate_no_use_phone()
register(phone, '12345678', _type=0)
res = login(phone, '12345678')
token = res['token_info']['token']
member_id = res['id']
loan = add_loan(member_id, token)
print(loan)


testcases.xlsx

 

test_audit.py
import json
import unittest

from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.make_requests import send_http_request
from common.fixture import register, login, add_loan
from common.test_data_handler import (
generate_no_use_phone, get_data_from_excel, replace_args_by_re)


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'audit')


@ddt
class TestAudit(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 审核接口 开始测试 =======')
# 1. 注册一个普通用户
mobile_phone = generate_no_use_phone()
pwd = '12345678'
register(mobile_phone, pwd)
# 2. 登录普通用户
data = login(mobile_phone, pwd)
# 3. 保存需要的数据
# 保存融资用户的member_id和token(融资用户创建项目需要用到member_id和token)
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册管理员用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, pwd, _type=0)
# 5. 登录管理员用户
data = login(mobile_phone, pwd)
# 6. 保存需要的数据(管理员用来对项目进行审核,以token区分管理员)
# 保存管理员用户的token
cls.token = data['token_info']['token']

def setUp(self) -> None:
# 1. 创建一个项目
data = add_loan(self.normal_member_id, self.normal_token)
# 2. 保存新的项目的id
# 通过对象属性保存项目id
self.loan_id = data['id']

@list_data(cases)
def test_audit(self, case):
# 1. 处理测试数据
# 1.1 生成动态数据并替换

# 1.2 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]
# 1.3 替换依赖参数
# 设计好的槽位 #token# #loan_id#
case['request_data'] = case['request_data'].replace('#token#', self.token)
case['request_data'] = case['request_data'].replace('#loan_id#', str(self.loan_id))
# # sql也需要替换
if case.get('sql'):
case['sql'] = case['sql'].replace('#loan_id#', str(self.loan_id))
# 1.4 将json格式的请求数据和期望数据转换为python对象
case['request_data'] = json.loads(case['request_data'])
case['expect_data'] = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**case['request_data']
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], case['expect_data']))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))
# 3.3 数据库断言
if case.get('sql'):
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))

2.3)动态参数替换

目的:解决不同用例替换参数代码不一致的问题。

 

2.3.1 设计思路

1. 用例数据中的槽位要和类/对象属性名一致
2. 找出用例数据中的槽位
3. 根据槽位依次去类中获取对应的属性并替换

 

2.3.2 封装动态参数替换函数

主要使用正则表达式

data_handler.py中封装动态参数替换函数
import re
import random

from openpyxl import load_workbook
from common import db


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


def generate_phone():
"""
随机的生成手机号码
:return:
"""
# 1开头
# 11位
# 第二个数字是3-9

# phone = ['1', str(random.randint(3, 9))]
phone = ['158']
for i in range(8):
phone.append(str(random.randint(0, 9)))
return ''.join(phone)


def generate_no_use_phone(sql="select id from member where mobile_phone='{}'"):
"""
生成没有注册的手机号码
:return:
"""
while True:
phone = generate_phone()

if not db.exist(sql.format(phone)):
return phone


def replace_args_by_re(s, obj):
"""
通过正则表达式动态替换参数
:param s: 需要被替换的参数字符串
:param obj: 提供数据的对象
:return:
"""
# 1. 先找出字符串中的槽位,返回一个列表 值为槽
args = re.findall('#(.*?)#', s)
# 2. 循环参数
for arg in args:
# 3. 获取obj对应参数名arg的属性,如果没有就None
value = getattr(obj, arg, None)
# 4. 替换同名参数的槽位为对应的value
if value:
s = s.replace('#{}#'.format(arg), str(value))
return s


if __name__ == '__main__':
s = '''{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #token#"},
"json":{"loan_id":#loan_id#,"approved_or_not":true}
}'''
class A:
pass
A.token = '我是token'
A.loan_id = 8888
res = replace_args_by_re(s, A)
print(res)
# res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
# print(res)
# print(generate_phone())
# print(generate_no_use_phone())

3.初代接口测试框架

 

1.项目结构

 

2.项目代码

1 common

__init__.py
# python中的每一个包下都会有一个__init__.py文件

# 当包被导入的时候,这个__init__.py文件里的代码会被执行,并且只会执行一遍

import settings
from common.log_handler import get_logger
from common.db_handler import SQLdbHandler

logger = get_logger(**settings.LOG_CONFIG)
db = SQLdbHandler(settings.DB_CONFIG)
db_handler.py
import pymysql
from pymysql.cursors import DictCursor


class SQLdbHandler:
"""
sql数据库查询类
"""
def __init__(self, db_config):
# 创建连接
# 根据不同的数据库,创建不同的链接
engine = db_config.pop('engine', 'mysql')
if engine.lower() == 'mysql':
self.conn = pymysql.connect(**db_config)

def get_one(self, sql, res_type='t'):
"""
获取一条数据
:param sql:
:param res_type: 返回数据的类型 默认为't表示以元组的形式返回
'd'表示以字典的形式返回
:return: 元组/字典
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchone()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchone()

def get_many(self, sql, size, res_type='t'):
"""
获取多条数据
:param sql:
:param size: 指定的条数
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)

def get_all(self, sql, res_type='t'):
"""
获取所有数据
:param sql:
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchall()

def exist(self, sql):
"""
查询数据是否存在
:param sql:
:return:
"""
if self.get_one(sql):
return True
else:
return False

def __del__(self):
"""
对象销毁的时候自动被调用
:return:
"""
self.conn.close()


if __name__ == '__main__':
import settings
db = SQLdbHandler(settings.DB_CONFIG)
sql = 'select leave_amount from member where id=10000000000'
res = db.get_one(sql, 'd')
print(res)
print(db.exist(sql))
fixture.py
"""
一些脚手架代码
"""
import requests

import settings
from common import logger


def register(mobile_phone, pwd, reg_name=None, _type=None):
"""
注册用户
:param mobile_phone:
:param pwd:
:param reg_name:
:param _type:
:return:
"""
# 1. 构造发送注册请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
if reg_name:
data['reg_name'] = reg_name
# 因为会传入0进来,所有不能直接if _type
if _type is not None:
data['type'] = _type

headers = {"X-Lemonban-Media-Type": "lemonban.v1"}
url = settings.INTERFACES['register']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('注册用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('注册用户失败')
raise e


def login(mobile_phone, pwd):
# 1. 构造发送登录请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
# v2才会返回token
headers = {"X-Lemonban-Media-Type": "lemonban.v2"}
url = settings.INTERFACES['login']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('登录用户成功')
return res.json()['data']
# 如果上面成功,就不会执行抛出报错信息
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


def add_loan(member_id, token, title='借钱实现财富自由', amount=5000, loan_rate=12.0,
loan_term=3, loan_date_type=1, bidding_days=5):
"""
添加一个项目
:param member_id:
:param token:
:param title:
:param amount:
:param loan_rate:
:param loan_term:
:param loan_date_type:
:param bidding_days:
:return:
"""
# 1. 构造发送添加项目请求的参数
data = {
'member_id': member_id,
'title': title,
'amount': amount,
'loan_rate': loan_rate,
'loan_term': loan_term,
'loan_date_type': loan_date_type,
'bidding_days': bidding_days
}
# 需要鉴权
headers = {"X-Lemonban-Media-Type": "lemonban.v2", 'Authorization': 'Bearer {}'.format(token)}
url = settings.INTERFACES['add']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('创建项目成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('创建项目失败')
raise e


if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone = generate_no_use_phone()
register(phone, '12345678', _type=0)
res = login(phone, '12345678')
token = res['token_info']['token']
member_id = res['id']
loan = add_loan(member_id, token)
print(loan)
log_handler.py
import logging
from logging.handlers import TimedRotatingFileHandler


def get_logger(name, filename, encoding='utf-8', fmt=None, when='d', interval=1, backup_count=7, debug=False):
"""

:param name: 日志器的名字
:param filename: 日志文件名(包含路径)
:param encoding: 字符编码
:param fmt: 日志格式
:param when: 日志轮转时间单位
:param interval: 间隔
:param backup_count: 日志文件个数
:param debug: 调试模式
:return:
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 文件处理器的等级一般情况一定比控制台要高
if debug:
file_level = logging.DEBUG
console_level = logging.DEBUG
else:
file_level = logging.WARNING
console_level = logging.INFO

if fmt is None:
fmt = '%(levelname)s %(asctime)s [%(filename)s-->line:%(lineno)d]:%(message)s'

file_handler = TimedRotatingFileHandler(
filename=filename, when=when, interval=interval, backupCount=backup_count, encoding=encoding)
file_handler.setLevel(file_level)

console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

return logger


if __name__ == '__main__':
import settings
log = get_logger(**settings.LOG_CONFIG)
log.info('我是普通信息')
log.warning('我是警告信息')
make_requests.py
"""
1. 输入参数 url, method, 以及需要携带的各种类型的http请求参数和请求头等
2. 可以使用动态关键字参数解决第一个步参数过的问题
3. 使用requests库请求方法的同名参数便于传递
4. 根据传入的method,发送对应的请求
"""
import requests


def send_http_request(url, method='get', **kwargs):
"""
发送http请求
:param url:
:param method:
:param kwargs: 接受requests库原生请求方法的关键字参数
:return:
"""
# 为了防止用户传入的方法名的大小写格式问题
# 统一一下大小写格式
method = method.lower()
# 根据方法名发送对应的请求
return getattr(requests, method)(url, **kwargs)

# kwargs = {'json': {'mobile_phone': '111111'}}
# if method == 'get':
# res = requests.get(url, **kwargs) # =》requests.get(url, json={'mobile_phone': '111111'})
# elif method == 'post':
# res = requests.post(url, **kwargs)
# elif method == 'put':
# res = requests.put(url, **kwargs)
# elif method == 'patch':
# res = requests.patch(url, **kwargs)
# elif method == 'delete':
# res = requests.delete(url, **kwargs)
# else:
# raise ValueError('请输入正确的请求方法!')
# return res


if __name__ == '__main__':

send_http_request('', 'post', json={'mobile_phone': '111111'})
data_handler.py
import re
import random

from openpyxl import load_workbook
from common import db


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


def generate_phone():
"""
随机的生成手机号码
:return:
"""
# 1开头
# 11位
# 第二个数字是3-9

# phone = ['1', str(random.randint(3, 9))]
phone = ['158']
for i in range(8):
phone.append(str(random.randint(0, 9)))
return ''.join(phone)


def generate_no_use_phone(sql="select id from member where mobile_phone='{}'"):
"""
生成没有注册的手机号码
:return:
"""
while True:
phone = generate_phone()

if not db.exist(sql.format(phone)):
return phone


def replace_args_by_re(s, obj):
"""
通过正则表达式动态替换参数
:param s: 需要被替换的参数字符串
:param obj: 提供数据的对象
:return:
"""
# 1. 先找出字符串中的槽位,返回一个列表 值为槽
args = re.findall('#(.*?)#', s)
# 2. 循环参数
for arg in args:
# 3. 获取obj对应参数名的属性
value = getattr(obj, arg, None)
# 4. 替换同名参数的槽位为对应的value
if value:
s = s.replace('#{}#'.format(arg), str(value))
return s


if __name__ == '__main__':
s = '''{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #token#"},
"json":{"loan_id":#loan_id#,"approved_or_not":true}
}'''
class A:
pass
A.token = '我是token'
A.loan_id = 8888
res = replace_args_by_re(s, A)
print(res)
# res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
# print(res)
# print(generate_phone())
# print(generate_no_use_phone())

2 logs

3 reports

4 test_data

testcases.xlsx

 

 

 

 

 

 

5 testcases

test_audit.py
import json
import unittest

from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.make_requests import send_http_request
from common.fixture import register, login, add_loan
from common.test_data_handler import (
generate_no_use_phone, get_data_from_excel, replace_args_by_re)


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'audit')


@ddt
class TestAudit(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 审核接口 开始测试 =======')
# 1. 注册一个普通用户
mobile_phone = generate_no_use_phone()
pwd = '12345678'
register(mobile_phone, pwd)
# 2. 登录普通用户
data = login(mobile_phone, pwd)
# 3. 保存需要的数据
# 保存融资用户的member_id和token(融资用户创建项目需要用到member_id和token)
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册管理员用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, pwd, _type=0)
# 5. 登录管理员用户
data = login(mobile_phone, pwd)
# 6. 保存需要的数据(管理员用来对项目进行审核,以token区分管理员)
# 保存管理员用户的token
cls.token = data['token_info']['token']

def setUp(self) -> None:
# 1. 创建一个项目
data = add_loan(self.normal_member_id, self.normal_token)
# 2. 保存新的项目的id
# 通过对象属性保存项目id
self.loan_id = data['id']

@list_data(cases)
def test_audit(self, case):
# 1. 处理测试数据
# 1.1 生成动态数据并替换

# 1.2 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]
# 1.3 替换依赖参数
# 设计好的槽位 #token# #loan_id#
case['request_data'] = replace_args_by_re(case['request_data'], self)
# # sql也需要替换
if case.get('sql'):
case['sql'] = replace_args_by_re(case['sql'], self)
# case['request_data'] = case['request_data'].replace('#token#', self.token)
# case['request_data'] = case['request_data'].replace('#loan_id#', str(self.loan_id))
# # sql也需要替换
# if case.get('sql'):
# case['sql'] = case['sql'].replace('#loan_id#', str(self.loan_id))
# 1.4 将json格式的请求数据和期望数据转换为python对象
case['request_data'] = json.loads(case['request_data'])
case['expect_data'] = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**case['request_data']
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], case['expect_data']))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))
# 3.3 数据库断言
if case.get('sql'):
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))
test_recharge.py
import json
import unittest

from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.fixture import register, login
from common.make_requests import send_http_request
from common.test_data_handler import (
generate_no_use_phone,
get_data_from_excel,
replace_args_by_re
)


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'recharge')


@ddt
class TestRecharge(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 充值接口 开始测试 =======')
# 1. 注册一个用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, '12345678')

# 2. 登录
data = login(mobile_phone, '12345678')

# 3. 绑定数据
# 绑定用户的id,绑定token
# 绑定到类属性中
cls.member_id = data['id']
cls.token = data['token_info']['token']

@classmethod
def tearDownClass(cls) -> None:
logger.info('========= 充值接口 测试结束 =======')

@list_data(cases)
def test_recharge(self, case):
# 1. 处理测试数据
# 1.1 生成动态数据并替换

# 1.2 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]
# 1.3 替换依赖参数
# # 设计好的槽位 #token# #member_id#
# case['request_data'] = case['request_data'].replace('#token#', self.token)
# case['request_data'] = case['request_data'].replace('#member_id#', str(self.member_id))
case['request_data'] = replace_args_by_re(case['request_data'], self)
# # sql也需要替换
if case.get('sql'):
case['sql'] = replace_args_by_re(case['sql'], self)
# if case.get('sql'):
# case['sql'] = case['sql'].replace('#member_id#', str(self.member_id))
# 1.4 将json格式的请求数据和期望数据转换为python对象
case['request_data'] = json.loads(case['request_data'])
case['expect_data'] = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**case['request_data']
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], case['expect_data']))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))
# 3.3 数据库断言
if case.get('sql'):
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))
test_register.py
import json
import unittest

import requests
from unittestreport import ddt, list_data

import settings
from common import logger, db
from common.test_data_handler import get_data_from_excel
from common.test_data_handler import generate_no_use_phone
from common.make_requests import send_http_request


cases = get_data_from_excel(settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(unittest.TestCase):

@classmethod
def setUpClass(cls) -> None:
logger.info('========= 注册接口 开始测试 =======')

@classmethod
def tearDownClass(cls) -> None:
logger.info('========= 注册接口 测试结束 =======')

@list_data(cases)
def test_register(self, case):
logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 1. 处理测试数据
# 1.1 生成动态数据并替换
if '$phone$' in case['request_data']:
# 如果request_data中存在生成数据的标志
phone = generate_no_use_phone()
# 立刻替换
case['request_data'] = case['request_data'].replace('$phone$', phone)
if case['sql']:
case['sql'] = case['sql'].replace('#phone#', phone)

# 1.2 拼接url
if 'http' in case['url']:
# 全地址
pass
elif '/' in case['url']:
case['url'] = settings.PROJECT_HOST + case['url']
else:
case['url'] = settings.INTERFACES[case['url']]

# 1.3 将请求数据和期望数据转换为python对象
case['request_data'] = json.loads(case['request_data'])
case['expect_data'] = json.loads(case['expect_data'])
# 2. 发送请求
# 根据测试用例数据来发送请求
response = send_http_request(
case['url'],
case['method'],
**case['request_data']
)
# 3. 断言
# 3.1 断言响应状态码
try:
self.assertEqual(case['status_code'], response.status_code)
except AssertionError as e:
logger.warning('用例【{}】响应状态码断言失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】响应状态码断言成功'.format(case['title']))

# 3.2 响应结果断言
# 判断响应数据类型
if case['res_type'] == 'json':
res = response.json()
elif case['res_type'] == 'xml':
pass
elif case['res_type'] == 'html':
pass

try:
self.assertEqual(case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
logger.warning('用例【{}】响应数据断言失败'.format(case['title']))
logger.info('用例【{}】期望的结果是{}'.format(case['title'], case['expect_data']))
logger.info('用例【{}】返回数据是{}'.format(case['title'], res))
raise e
else:
logger.info('用例【{}】响应数据断言成功'.format(case['title']))

# 3.3 数据库断言
if case['sql']:
logger.info('用例【{}】数据校验的sql为:{}'.format(case['title'], case['sql']))
try:
self.assertTrue(db.exist(case['sql']))
except AssertionError as e:
logger.warning('用例【{}】数据库断言失败'.format(case['title']))
raise e
except Exception as e:
logger.warning('用例【{}】数据库查询失败'.format(case['title']))
raise e
else:
logger.info('用例【{}】数据断言成功'.format(case['title']))


6 main.py

import unittest

from unittestreport import TestRunner

import settings


if __name__ == '__main__':
# 1. 收集用例
ts = unittest.defaultTestLoader.discover(settings.TESTCASES_DIR)
# 2. 运行并生成测试报告

TestRunner(ts, **settings.REPORT_CONFIG).run()

7 settings.py

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
# =================== 项目模块 =========================
'add': PROJECT_HOST + '/loan/add',
'audit': PROJECT_HOST + '/loan/audit',
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'futureloan', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}


 

 

第 5 章. 测试用例基类抽取

1.抽取思路

  1. 将公用模块都封装到基类中便于子类直接调用

    • 日志器

    • 数据库查询模块

    • 项目配置

  2. 将测试方法中每一个单独步骤封装成对象方法

    • 处理测试数据

    • 发送请求

    • 断言

  3. 按照第二步的思想可以进一步将小步骤单独抽出成更小的功能方法

好处:

  1. 代码复用

  2. 维护升级方便

 

第 6 章. 业务流

进行单接口测试之前,需要先测通核心业务流

1.接口测试业务流设计

  1. 站在用户的角度,从产品业务出发

  2. 重视全局而非细节

  3. 先测主流程,后测子流程

  4. 只测正例

2.前程贷业务流

投资流程

  1. 注册普通融资用户

  2. 登录普通融资用户

  3. 创建融资项目

  4. 注册管理员用户

  5. 登录管理员用户

  6. 审核项目

  7. 注册普通投资用户

  8. 登录普通投资用户

  9. 充值

  10. 投资

 

3.代码实现

3.1 方案1-⭐二代接口测试框架

  1. 在测试用例类中根据业务流,按顺序一个请求定义一个对应的单元测试方法

  2. 并将后面接口需要用到的数据绑定到类属性中进行传递

优点:逻辑清晰

缺点:

  1. 业务流很长的时候代码量很大

  2. 业务发生改变需要修改代码

 

 

3.1.1 增加base_case.py 抽取基类

⭐测试用例不继承unittest.TestCase,base_case.py继承unittest.TestCase,测试用例直接继承BaseCase

⭐不封装为私有方法的,在公司不同项目中,可以根据实际情况重写

base_case.py
import json
import unittest

import settings
from common import db, logger
from common.make_requests import send_http_request
from common.test_data_handler import (
generate_no_use_phone,
replace_args_by_re)


class BaseCase(unittest.TestCase):
"""
测试基类
"""
# 将公用模块绑定到类属性上
db = db
logger = logger
settings = settings
name = 'base接口'

@classmethod
def setUpClass(cls) -> None:
cls.logger.info('========= {} 开始测试 ======='.format(cls.name))

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('========= {} 测试结束 ======='.format(cls.name))

def step(self, case):
"""
测试步骤,三大步骤,case为ddt传递过来的每一条测试用例
:param case:
:return:
"""

self.logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 绑定case到对象属性上便于其他对象方法访问
self.case = case
self.__process_test_data() # 1.处理测试数据

self.send_http_request() # 2.发送请求

self.__assert_res() # 3.断言
self.logger.info('********** 用例【{}】 测试成功 *************'.format(case['title']))

# 1.1 生成动态数据并替换

def generate_test_data(self):
"""
动态生成测试数据 phone
:return:
"""
# 子类可以复写实现自定义的生成测试数据
# 1 生成动态数据并替换
try:
if '$phone$' in self.case['request_data']:
# 如果request_data中存在生成数据的标志
phone = generate_no_use_phone()
# 立刻替换
self.case['request_data'] = self.case['request_data'].replace('$phone$', phone)
          # 用例case里面可能没有SQL,所以要用get,如果没有获取到的是null,就不会执行if语句,而不是报错
if self.case.get('sql'):
self.case['sql'] = self.case['sql'].replace('#phone#', phone)
except Exception as e:
self.logger.warning('用例【{}】生成测试数据$phone$的时候失败'.format(self.case['title']))
raise e

# 1.2 url的处理

def __process_url(self):
"""
url的处理
:return:
"""
try:
if 'http' in self.case['url']:
# 全地址
pass
elif '/' in self.case['url']:
self.case['url'] = self.settings.PROJECT_HOST + self.case['url']
else:
self.case['url'] = self.settings.INTERFACES[self.case['url']]
except Exception as e:
self.logger.warning('用例【{}】在拼接处理url的时候失败'.format(self.case['title']))
raise e

# 1.3 替换参数

def __replace_args(self):
"""
替换参数
:return:
"""
try:
self.case['url'] = replace_args_by_re(self.case['url'], self)
self.case['request_data'] = replace_args_by_re(self.case['request_data'], self)
# sql也需要替换
if self.case.get('sql'):
self.case['sql'] = replace_args_by_re(self.case['sql'], self)
except Exception as e:
self.logger.warning('用例【{}】替换参数时失败'.format(self.case['title']))
raise e

# 1.处理测试数据
def __process_test_data(self):
"""
处理测试数据
:return:
"""

# 1.1 生成测试数据
self.generate_test_data()
# 1.2 url处理
self.__process_url()
# 1.3 替换依赖参数
self.__replace_args()
# 1.4 将请求数据和期望数据转换为python对象
try:
self.case['request_data'] = json.loads(self.case['request_data'])
self.case['expect_data'] = json.loads(self.case['expect_data'])
except Exception as e:
self.logger.warning('用例【{}】在请求参数反序列化时失败'.format(self.case['title']))
raise e

# 2. 发送请求
def send_http_request(self):
"""
发送http请求
:return:
"""
# 2. 发送请求
# 根据测试用例数据来发送请求
try:
self.response = send_http_request(
self.case['url'],
self.case['method'],
**self.case['request_data']
)
except Exception as e:
self.logger.warning('用例【{}】在发送请求时失败'.format(self.case['title']))
raise e

# 3.1 响应状态码断言

def __assert_status_code(self):
"""
响应状态码断言
:return:
"""
try:
self.assertEqual(self.case['status_code'], self.response.status_code)
except AssertionError as e:
self.logger.warning('用例【{}】响应状态码断言失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】响应状态码断言成功'.format(self.case['title']))

# 3.2 响应信息断言

def assert_response(self):
"""
断言响应数据
:return:
"""
if self.case.get('res_type') == 'json':
res = self.response.json()
elif self.case.get('res_type') == 'xml':
pass
elif self.case.get('res_type') == 'html':
pass

try:
self.assertEqual(self.case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
self.logger.warning('用例【{}】响应数据断言失败'.format(self.case['title']))
self.logger.info('用例【{}】期望的结果是{}'.format(self.case['title'], self.case['expect_data']))
self.logger.info('用例【{}】返回数据是{}'.format(self.case['title'], res))
raise e
else:
self.logger.info('用例【{}】响应数据断言成功'.format(self.case['title']))

# 3.3 响应数据库断言

def assert_db(self):
"""
数据库断言
:return:
"""
if self.case.get('sql'):
self.logger.info('用例【{}】数据校验的sql为:{}'.format(self.case['title'], self.case['sql']))
try:
self.assertTrue(self.db.exist(self.case['sql']))
except AssertionError as e:
self.logger.warning('用例【{}】数据库断言失败'.format(self.case['title']))
raise e
except Exception as e:
self.logger.warning('用例【{}】数据库查询失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】数据断言成功'.format(self.case['title']))

# 3.断言

def __assert_res(self):
"""
断言
:return:
"""
# 3. 断言
# 3.1 断言响应状态码
self.__assert_status_code()

# 3.2 响应结果断言
# 判断响应数据类型
self.assert_response()

# 3.3 数据库断言
self.assert_db()


__init__.py
# python中的每一个包下都会有一个__init__.py文件

# 当包被导入的时候,这个__init__.py文件里的代码会被执行,并且只会执行一遍
import unittest
import settings
from common.log_handler import get_logger
from common.db_handler import SQLdbHandler

logger = get_logger(**settings.LOG_CONFIG)
db = SQLdbHandler(settings.DB_CONFIG)
db_handler.py
import pymysql
from pymysql.cursors import DictCursor


class SQLdbHandler:
"""
sql数据库查询类
"""
def __init__(self, db_config):
# 创建连接
# 根据不同的数据库,创建不同的链接
engine = db_config.pop('engine', 'mysql')
if engine.lower() == 'mysql':
self.conn = pymysql.connect(**db_config)

def get_one(self, sql, res_type='t'):
"""
获取一条数据
:param sql:
:param res_type: 返回数据的类型 默认为't表示以元组的形式返回
'd'表示以字典的形式返回
:return: 元组/字典
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchone()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchone()

def get_many(self, sql, size, res_type='t'):
"""
获取多条数据
:param sql:
:param size: 指定的条数
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)

def get_all(self, sql, res_type='t'):
"""
获取所有数据
:param sql:
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchall()

def exist(self, sql):
"""
查询数据是否存在
:param sql:
:return:
"""
if self.get_one(sql):
return True
else:
return False

def __del__(self):
"""
对象销毁的时候自动被调用
:return:
"""
self.conn.close()


if __name__ == '__main__':
import settings
db = SQLdbHandler(settings.DB_CONFIG)
sql = 'select leave_amount from member where id=10000000000'
res = db.get_one(sql, 'd')
print(res)
print(db.exist(sql))
fixture.py
"""
一些脚手架代码
"""
import requests

import settings
from common import logger


def register(mobile_phone, pwd, reg_name=None, _type=None):
"""
注册用户
:param mobile_phone:
:param pwd:
:param reg_name:
:param _type:
:return:
"""
# 1. 构造发送注册请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
if reg_name:
data['reg_name'] = reg_name
# 因为会传入0进来,所有不能直接if _type
if _type is not None:
data['type'] = _type

headers = {"X-Lemonban-Media-Type": "lemonban.v1"}
url = settings.INTERFACES['register']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('注册用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('注册用户失败')
raise e


def login(mobile_phone, pwd):
# 1. 构造发送登录请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
# v2才会返回token
headers = {"X-Lemonban-Media-Type": "lemonban.v2"}
url = settings.INTERFACES['login']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('登录用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


def add_loan(member_id, token, title='借钱实现财富自由', amount=5000, loan_rate=12.0,
loan_term=3, loan_date_type=1, bidding_days=5):
"""
添加一个项目
:param member_id:
:param token:
:param title:
:param amount:
:param loan_rate:
:param loan_term:
:param loan_date_type:
:param bidding_days:
:return:
"""
# 1. 构造发送登录请求的参数
data = {
'member_id': member_id,
'title': title,
'amount': amount,
'loan_rate': loan_rate,
'loan_term': loan_term,
'loan_date_type': loan_date_type,
'bidding_days': bidding_days
}
# 需要鉴权
headers = {"X-Lemonban-Media-Type": "lemonban.v2", 'Authorization': 'Bearer {}'.format(token)}
url = settings.INTERFACES['add']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('创建项目成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone = generate_no_use_phone()
register(phone, '12345678', _type=0)
res = login(phone, '12345678')
token = res['token_info']['token']
member_id = res['id']
loan = add_loan(member_id, token)
print(loan)
log_handler.py
import logging
from logging.handlers import TimedRotatingFileHandler


def get_logger(name, filename, encoding='utf-8', fmt=None, when='d', interval=1, backup_count=7, debug=False):
"""

:param name: 日志器的名字
:param filename: 日志文件名(包含路径)
:param encoding: 字符编码
:param fmt: 日志格式
:param when: 日志轮转时间单位
:param interval: 间隔
:param backup_count: 日志文件个数
:param debug: 调试模式
:return:
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 文件处理器的等级一般情况一定比控制台要高
if debug:
file_level = logging.DEBUG
console_level = logging.DEBUG
else:
file_level = logging.WARNING
console_level = logging.INFO

if fmt is None:
fmt = '%(levelname)s %(asctime)s [%(filename)s-->line:%(lineno)d]:%(message)s'

file_handler = TimedRotatingFileHandler(
filename=filename, when=when, interval=interval, backupCount=backup_count, encoding=encoding)
file_handler.setLevel(file_level)

console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

return logger


if __name__ == '__main__':
import settings
log = get_logger(**settings.LOG_CONFIG)
log.info('我是普通信息')
log.warning('我是警告信息')
make_requests.py
"""

1. 输入参数 url, method, 以及需要携带的各种类型的http请求参数和请求头等
2. 可以使用动态关键字参数解决第一个步参数过的问题
3. 使用requests库请求方法的同名参数便于传递
4. 根据传入的method,发送对应的请求
"""
import requests


def send_http_request(url, method='get', **kwargs):
"""
发送http请求
:param url:
:param method:
:param kwargs: 接受requests库原生请求方法的关键字参数
:return:
"""
# 为了防止用户传入的方法名的大小写格式问题
# 统一一下大小写格式
method = method.lower()
# 根据方法名发送对应的请求
return getattr(requests, method)(url, **kwargs)

 

data_handler.py
import re
import random

from openpyxl import load_workbook
from common import db


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


def generate_phone():
"""
随机的生成手机号码
:return:
"""
# 1开头
# 11位
# 第二个数字是3-9

# phone = ['1', str(random.randint(3, 9))]
phone = ['158']
for i in range(8):
phone.append(str(random.randint(0, 9)))
return ''.join(phone)


def generate_no_use_phone(sql="select id from member where mobile_phone='{}'"):
"""
生成没有注册的手机号码
:return:
"""
while True:
phone = generate_phone()

if not db.exist(sql.format(phone)):
return phone


def replace_args_by_re(s, obj):
"""
通过正则表达式动态替换参数
:param s: 需要被替换的参数字符串
:param obj: 提供数据的对象
:return:
"""
# 1. 先找出字符串中的槽位
args = re.findall('#(.*?)#', s)
# 2. 循环参数
for arg in args:
# 3. 获取obj对应参数名的属性
value = getattr(obj, arg, None)
# 4. 替换
if value:
s = s.replace('#{}#'.format(arg), str(value))
return s


if __name__ == '__main__':
s = '''{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #aaa#"},
"json":{"loan_id":#bbb#,"approved_or_not":true}
}'''
class A:
pass
A.token = '我是token'
A.loan_id = 8888
res = replace_args_by_re(s, A)
print(res)
# res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
# print(res)
# print(generate_phone())
# print(generate_no_use_phone())

 

1.2 修改所有测试用例代码

test_register.py
from unittestreport import ddt, list_data

from common.test_data_handler import get_data_from_excel
from common.base_case import BaseCase

cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(BaseCase):
name = '注册接口'

@list_data(cases)
def test_register(self, case):

self.step(case)


if __name__ == '__main__':
import unittest
unittest.main()

test_recharge.py
rom unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.fixture import register, login
from common.test_data_handler import (
generate_no_use_phone,
get_data_from_excel,
)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'recharge')


@ddt
class TestRecharge(BaseCase):
name = '充值接口'

@classmethod
def setUpClass(cls) -> None:
# 执行下父类的方法
super().setUpClass()
# 1. 注册一个用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, '12345678')

# 2. 登录
data = login(mobile_phone, '12345678')

# 3. 绑定数据
# 绑定用户的id,绑定token
# 绑定到类属性中
cls.member_id = data['id']
cls.token = data['token_info']['token']

@list_data(cases)
def test_recharge(self, case):
self.step(case)

test_aduit.py
from unittestreport import ddt, list_data

from common.base_case import BaseCase

from common.fixture import register, login, add_loan
from common.test_data_handler import (
generate_no_use_phone, get_data_from_excel)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'audit')


@ddt
class TestAudit(BaseCase):
name = '审核接口'

@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
# 1. 注册一个普通用户
mobile_phone = generate_no_use_phone()
pwd = '12345678'
register(mobile_phone, pwd)
# 2. 登录普通用户
data = login(mobile_phone, pwd)
# 3. 保存需要的数据
# 保存融资用户的member_id和token
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册管理员用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, pwd, _type=0)
# 5. 登录管理员用户
data = login(mobile_phone, pwd)
# 6. 保存需要的数据
# 保存管理员用户的token
cls.token = data['token_info']['token']

def setUp(self) -> None:
# 1. 创建一个项目
data = add_loan(self.normal_member_id, self.normal_token)
# 2. 保存新的项目的id
# 通过对象属性保存项目id
self.loan_id = data['id']

@list_data(cases)
def test_audit(self, case):
self.step(case)

1.3 settings里面增加invest接口url

settings.py
import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
'invest': PROJECT_HOST + '/member/invest',
# =================== 项目模块 =========================
'add': PROJECT_HOST + '/loan/add',
'audit': PROJECT_HOST + '/loan/audit'

}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'future', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}

1.4 编写test_flow.py 业务流测试

demo.py
import unittest


class TestSome(unittest.TestCase):

def test_01(self):
self.__class__.name = '小白'
# self.name = '小白'

def test_02(self):
print(self.name)

# 测试用例实例化
# TestSome('test_01')
#
# TestSome('test_02')

test_flow.py
from common.base_case import BaseCase


class TestFlow(BaseCase):
name = '投资业务流'

def test_01register_normal_user(self):
"""
注册普通用户
:return:
"""
# 每一个用例都需要有测试用例数据case
case = {
'title': '注册普通用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 将手机号码传递给下一个测试函数
# 可以用对象属性吗?不能,
# 因为每一个单元测试函数都会单独实例化一个用例,两个用例之间是单独的个体,A用例定义的属性,B用例不能访问,但是两个用例有共同的类爸爸
# 所以可以绑定在类属性上
     # 见demo
# 正常运行后,case里面的标志位$phone$就会被电话号码替换掉
self.__class__.normal_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_02login_normal_user(self):
"""
登录普通用户
"""
case = {
'title': '登录普通用户',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#normal_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定用户id,token属性
self.__class__.normal_member_id = self.response.json()['data']['id']
self.__class__.normal_token = self.response.json()['data']['token_info']['token']

def test_03add_loan(self):
"""
创建融资项目
:return:
"""
case = {
'title': '添加项目',
'url': 'add',
'method': 'post',
'request_data': '''
{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #normal_token#"},
"json":{
"member_id":#normal_member_id#,
"title":"实现财富自由",
"amount":5000,
"loan_rate":18.0,
"loan_term":6,
"loan_date_type":1,
"bidding_days":10}
}
''',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定属性 如果通过了,绑定loan_id
self.__class__.loan_id = self.response.json()['data']['id']

def test_04register_admin_user(self):
"""
注册管理员用户
:return:
"""
case = {
'title': '注册管理员用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678","type":0}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
self.__class__.admin_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_05login_admin_user(self):
"""
登录管理员用户
:return:
"""
case = {
'title': '登录管理员用户',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#admin_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定用户id,token属性
self.__class__.admin_token = self.response.json()['data']['token_info']['token']

def test_06audit_loan(self):
"""
审核项目
:return:
"""
case = {
'title': '审核项目',
'url': 'audit',
'method': 'patch',
'request_data': '''
{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #admin_token#"},
"json":{"loan_id":#loan_id#,"approved_or_not":true}
}
''',

'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)

def test_07register_invest_user(self):
"""
注册投资用户
:return:
"""
case = {
'title': '注册投资用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678","type":1}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
self.__class__.invest_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_08login_invest_user(self):
"""
登录投资用户
:return:
"""
case = {
'title': '登录投资用户',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#invest_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定用户id,token属性
self.__class__.invest_member_id = self.response.json()['data']['id']
self.__class__.invest_token = self.response.json()['data']['token_info']['token']

def test_09invest_user_recharge(self):
"""
投资用户充值
:return:
"""
case = {
'title': '投资用户充值',
'url': 'recharge',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2",'
'"Authorization":"Bearer #invest_token#"},'
'"json": {"member_id":#invest_member_id#,"amount":5000}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)

def test_10invest(self):
"""
投资
:return:
"""
case = {
'title': '投资',
'url': 'invest',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2",'
'"Authorization":"Bearer #invest_token#"},'
'"json":{"member_id":#invest_member_id#,"loan_id":#loan_id#,"amount":1000}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)


main.py
import unittest

from unittestreport import TestRunner

import settings


if __name__ == '__main__':
# 1. 收集用例
ts = unittest.defaultTestLoader.discover(settings.TESTCASES_DIR)
# 2. 运行并生成测试报告

TestRunner(ts, **settings.REPORT_CONFIG).run()


⭐测试用例数据不变 

⭐ctd模式框架

c - 工具层 - common

t - 测试用例层 - testcases

d - 测试数据层 - test_data

 

 

3.2 方案2 ⭐三代接口测试框架-业务流ddt

思路:把业务流也做成ddt

优点:

  1. 代码复用,基本流程不变,基本上可以一劳永逸

  2. 当业务流发生改变的时候,只需要修改用例数据而不用修改代码    

缺点:

  1. 学习成本高,需要对代码的逻辑了解清楚才能够编写用例

 

1.用例数据的设计

在用例数据中添加一列extract表示要提取响应数据并保存到对应的类属性中,规则如下:

[['类属性名1', '提取表达式1'],['类属性名2', '提取表达式2']]

上面的提取规则是一个嵌套的数组,每个子数组的第一个元素表示要绑定到类中的属性名,第二个元素表示从json响应数据提取数据的jsonpath表达式。

 

2.jsonpath模块

pip install jsonpath 官方地址

 

 

 

 

 

 

 

 

 

 

data = { "store":{
"book":[
{ "category":"参考",
"author":"Nigel Rees",
"title":"世纪风俗",
"price":8.95
},
{ "category":"小说",
"author":"Evelyn Waugh",
"title":"荣誉剑",
"price":12.99
},
{ "category":"小说",
"author":"Herman Melville",
"title":"Moby Dick",
"isbn":"0-553-21311-3",
"price":8.99
},
{ "category":"小说",
"author":"JRR Tolkien",
"title":"指环王",
"isbn":"0-395-19395-8",
"price":22.99
}
],
"bicycle":{
"color":"red",
"price":19.95
}
}
}

 

 

 

 

 

 

 

 

3. 提取函数封装

功能分析:

  1. 根据jsonpath表达式提取响应结果中的数据

  2. 动态的绑定数据到指定的对象

 

 4. 三代接口框架-业务流ddt

 

❀test_data_handler.py

中增加测试提取jsonpath


import json
import re
import random

from jsonpath import jsonpath

from openpyxl import load_workbook
from common import db


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


def generate_phone():
"""
随机的生成手机号码
:return:
"""
# 1开头
# 11位
# 第二个数字是3-9

# phone = ['1', str(random.randint(3, 9))]
phone = ['158']
for i in range(8):
phone.append(str(random.randint(0, 9)))
return ''.join(phone)


def generate_no_use_phone(sql="select id from member where mobile_phone='{}'"):
"""
生成没有注册的手机号码
:return:
"""
while True:
phone = generate_phone()

if not db.exist(sql.format(phone)):
return phone


def replace_args_by_re(s, obj):
"""
通过正则表达式动态替换参数
:param s: 需要被替换的参数字符串
:param obj: 提供数据的对象
:return:
"""
# 1. 先找出字符串中的槽位
args = re.findall('#(.*?)#', s)
# 2. 循环参数
for arg in args:
# 3. 获取obj对应参数名的属性
value = getattr(obj, arg, None)
# 4. 替换
if value:
s = s.replace('#{}#'.format(arg), str(value))
return s


def extract_data(rule, json_data, obj):
"""
根据提取表达式提取响应数据中的值并绑定到obj的对应属性上
:param rule: 提取规则
:param json_data:
:param obj:
:return:
"""
data = json.loads(json_data)

name = rule[0] # 要绑定的属性名
exp = rule[1] # jsonpath 表达式
value = jsonpath(data, exp) # 在响应数据中去提取数据
if value:
# 如果提取到了数据就绑定到类属性中
setattr(obj, name, value[0])
else:
raise ValueError('用例的提取表达式{}提取不到数据'.format(rule))


if __name__ == '__main__':
rule = ["normal_mobile_phone", "$..mobile_phone"]

class A:
pass

data = '{"mobile_phone": "15873061798","pwd": "123456"}'

extract_data(rule, data, A)
print(A.normal_mobile_phone)
# s = '''{
# "headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #aaa#"},
# "json":{"loan_id":#bbb#,"approved_or_not":true}
# }'''
# class A:
# pass
# A.token = '我是token'
# A.loan_id = 8888
# res = replace_args_by_re(s, A)
# print(res)
# res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
# print(res)
# print(generate_phone())
# print(generate_no_use_phone())

__init__.py
# python中的每一个包下都会有一个__init__.py文件

# 当包被导入的时候,这个__init__.py文件里的代码会被执行,并且只会执行一遍
import unittest
import settings
from common.log_handler import get_logger
from common.db_handler import SQLdbHandler

logger = get_logger(**settings.LOG_CONFIG)
db = SQLdbHandler(settings.DB_CONFIG)
db_handler.py

import pymysql
from pymysql.cursors import DictCursor


class SQLdbHandler:
"""
sql数据库查询类
"""
def __init__(self, db_config):
# 创建连接
# 根据不同的数据库,创建不同的链接
engine = db_config.pop('engine', 'mysql')
if engine.lower() == 'mysql':
self.conn = pymysql.connect(**db_config)

def get_one(self, sql, res_type='t'):
"""
获取一条数据
:param sql:
:param res_type: 返回数据的类型 默认为't表示以元组的形式返回
'd'表示以字典的形式返回
:return: 元组/字典
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchone()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchone()

def get_many(self, sql, size, res_type='t'):
"""
获取多条数据
:param sql:
:param size: 指定的条数
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)

def get_all(self, sql, res_type='t'):
"""
获取所有数据
:param sql:
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchall()

def exist(self, sql):
"""
查询数据是否存在
:param sql:
:return:
"""
if self.get_one(sql):
return True
else:
return False

def __del__(self):
"""
对象销毁的时候自动被调用
:return:
"""
self.conn.close()


if __name__ == '__main__':
import settings
db = SQLdbHandler(settings.DB_CONFIG)
sql = 'select leave_amount from member where id=10000000000'
res = db.get_one(sql, 'd')
print(res)
print(db.exist(sql))

fixture.py

"""
一些脚手架代码
"""
import requests

import settings
from common import logger


def register(mobile_phone, pwd, reg_name=None, _type=None):
"""
注册用户
:param mobile_phone:
:param pwd:
:param reg_name:
:param _type:
:return:
"""
# 1. 构造发送注册请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
if reg_name:
data['reg_name'] = reg_name
# 因为会传入0进来,所有不能直接if _type
if _type is not None:
data['type'] = _type

headers = {"X-Lemonban-Media-Type": "lemonban.v1"}
url = settings.INTERFACES['register']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('注册用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('注册用户失败')
raise e


def login(mobile_phone, pwd):
# 1. 构造发送登录请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
# v2才会返回token
headers = {"X-Lemonban-Media-Type": "lemonban.v2"}
url = settings.INTERFACES['login']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('登录用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


def add_loan(member_id, token, title='借钱实现财富自由', amount=5000, loan_rate=12.0,
loan_term=3, loan_date_type=1, bidding_days=5):
"""
添加一个项目
:param member_id:
:param token:
:param title:
:param amount:
:param loan_rate:
:param loan_term:
:param loan_date_type:
:param bidding_days:
:return:
"""
# 1. 构造发送登录请求的参数
data = {
'member_id': member_id,
'title': title,
'amount': amount,
'loan_rate': loan_rate,
'loan_term': loan_term,
'loan_date_type': loan_date_type,
'bidding_days': bidding_days
}
# 需要鉴权
headers = {"X-Lemonban-Media-Type": "lemonban.v2", 'Authorization': 'Bearer {}'.format(token)}
url = settings.INTERFACES['add']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('创建项目成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone = generate_no_use_phone()
register(phone, '12345678', _type=0)
res = login(phone, '12345678')
token = res['token_info']['token']
member_id = res['id']
loan = add_loan(member_id, token)
print(loan)
log_handler.py


import logging
from logging.handlers import TimedRotatingFileHandler


def get_logger(name, filename, encoding='utf-8', fmt=None, when='d', interval=1, backup_count=7, debug=False):
"""

:param name: 日志器的名字
:param filename: 日志文件名(包含路径)
:param encoding: 字符编码
:param fmt: 日志格式
:param when: 日志轮转时间单位
:param interval: 间隔
:param backup_count: 日志文件个数
:param debug: 调试模式
:return:
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 文件处理器的等级一般情况一定比控制台要高
if debug:
file_level = logging.DEBUG
console_level = logging.DEBUG
else:
file_level = logging.WARNING
console_level = logging.INFO

if fmt is None:
fmt = '%(levelname)s %(asctime)s [%(filename)s-->line:%(lineno)d]:%(message)s'

file_handler = TimedRotatingFileHandler(
filename=filename, when=when, interval=interval, backupCount=backup_count, encoding=encoding)
file_handler.setLevel(file_level)

console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

return logger


if __name__ == '__main__':
import settings
log = get_logger(**settings.LOG_CONFIG)
log.info('我是普通信息')
log.warning('我是警告信息')


make_requests.py
"""

1. 输入参数 url, method, 以及需要携带的各种类型的http请求参数和请求头等
2. 可以使用动态关键字参数解决第一个步参数过的问题
3. 使用requests库请求方法的同名参数便于传递
4. 根据传入的method,发送对应的请求
"""
import requests


def send_http_request(url, method='get', **kwargs):
"""
发送http请求
:param url:
:param method:
:param kwargs: 接受requests库原生请求方法的关键字参数
:return:
"""
# 为了防止用户传入的方法名的大小写格式问题
# 统一一下大小写格式
method = method.lower()
# 根据方法名发送对应的请求
return getattr(requests, method)(url, **kwargs)

# kwargs = {'json': {'mobile_phone': '111111'}}
# if method == 'get':
# res = requests.get(url, **kwargs) # =》requests.get(url, json={'mobile_phone': '111111'})
# elif method == 'post':
# res = requests.post(url, **kwargs)
# elif method == 'put':
# res = requests.put(url, **kwargs)
# elif method == 'patch':
# res = requests.patch(url, **kwargs)
# elif method == 'delete':
# res = requests.delete(url, **kwargs)
# else:
# raise ValueError('请输入正确的请求方法!')
# return res


if __name__ == '__main__':

send_http_request('', 'post', json={'mobile_phone': '111111'})

 

❀base_case.py 中 |封装提取函数|
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2021/8/3 20:08
# @Author : 心蓝
import json
import unittest

from jsonpath import jsonpath

import settings
from common import db, logger
from common.make_requests import send_http_request
from common.test_data_handler import (
generate_no_use_phone,
replace_args_by_re)


class BaseCase(unittest.TestCase):
"""
测试基类
"""
# 将公用模块绑定到类属性上
db = db
logger = logger
settings = settings
name = 'base接口'

@classmethod
def setUpClass(cls) -> None:
cls.logger.info('========= {} 开始测试 ======='.format(cls.name))

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('========= {} 测试结束 ======='.format(cls.name))

def step(self, case):
"""
测试步骤
:param case:
:return:
"""

self.logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 绑定case到对象属性上便于其他对象方法访问
self.case = case
# 1. 处理测试数据
self.__process_test_data()
# 2. 发送请求
self.send_http_request()
# 3. 断言
self.__assert_res()
# 4. 提取依赖数据
self.__extract_data()
self.logger.info('********** 用例【{}】 测试成功 *************'.format(case['title']))

def generate_test_data(self):
"""
动态生成测试数据 phone
:return:
"""
# 子类可以复写实现自定义的生成测试数据
# 1 生成动态数据并替换
try:
if '$phone$' in self.case['request_data']:
# 如果request_data中存在生成数据的标志
phone = generate_no_use_phone()
# 立刻替换
self.case['request_data'] = self.case['request_data'].replace('$phone$', phone)
if self.case.get('sql'):
self.case['sql'] = self.case['sql'].replace('#phone#', phone)
except Exception as e:
self.logger.warning('用例【{}】生成测试数据$phone$的时候失败'.format(self.case['title']))
raise e

def __process_url(self):
"""
url的处理
:return:
"""
try:
if 'http' in self.case['url']:
# 全地址
pass
elif '/' in self.case['url']:
self.case['url'] = self.settings.PROJECT_HOST + self.case['url']
else:
self.case['url'] = self.settings.INTERFACES[self.case['url']]
except Exception as e:
self.logger.warning('用例【{}】在拼接处理url的时候失败'.format(self.case['title']))
raise e

def __replace_args(self):
"""
替换参数
:return:
"""
try:
self.case['url'] = replace_args_by_re(self.case['url'], self)
self.case['request_data'] = replace_args_by_re(self.case['request_data'], self)
# sql也需要替换
if self.case.get('sql'):
self.case['sql'] = replace_args_by_re(self.case['sql'], self)
except Exception as e:
self.logger.warning('用例【{}】替换参数时失败'.format(self.case['title']))
raise e

def __process_test_data(self):
"""
处理测试数据
:return:
"""

# 1. 生成测试数据
self.generate_test_data()
# 2. url处理
self.__process_url()
# 3. 替换依赖参数
self.__replace_args()

# 4. 将请求数据和期望数据转换为python对象
try:
self.case['request_data'] = json.loads(self.case['request_data'])
self.case['expect_data'] = json.loads(self.case['expect_data'])
except Exception as e:
self.logger.warning('用例【{}】在请求参数反序列化时失败'.format(self.case['title']))
raise e

def send_http_request(self):
"""
发送http请求
:return:
"""
# 2. 发送请求
# 根据测试用例数据来发送请求
try:
self.response = send_http_request(
self.case['url'],
self.case['method'],
**self.case['request_data']
)
except Exception as e:
self.logger.warning('用例【{}】在发送请求时失败'.format(self.case['title']))
raise e

def __assert_status_code(self):
"""
响应状态码断言
:return:
"""
try:
self.assertEqual(self.case['status_code'], self.response.status_code)
except AssertionError as e:
self.logger.warning('用例【{}】响应状态码断言失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】响应状态码断言成功'.format(self.case['title']))

def assert_response(self):
"""
断言响应数据
:return:
"""
if self.case.get('res_type') == 'json':
res = self.response.json()
elif self.case.get('res_type') == 'xml':
pass
elif self.case.get('res_type') == 'html':
pass

try:
self.assertEqual(self.case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
self.logger.warning('用例【{}】响应数据断言失败'.format(self.case['title']))
self.logger.info('用例【{}】期望的结果是{}'.format(self.case['title'], self.case['expect_data']))
self.logger.info('用例【{}】返回数据是{}'.format(self.case['title'], res))
raise e
else:
self.logger.info('用例【{}】响应数据断言成功'.format(self.case['title']))

def assert_db(self):
"""
数据库断言
:return:
"""
if self.case.get('sql'):
self.logger.info('用例【{}】数据校验的sql为:{}'.format(self.case['title'], self.case['sql']))
try:
self.assertTrue(self.db.exist(self.case['sql']))
except AssertionError as e:
self.logger.warning('用例【{}】数据库断言失败'.format(self.case['title']))
raise e
except Exception as e:
self.logger.warning('用例【{}】数据库查询失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】数据断言成功'.format(self.case['title']))

def __extract_data_from_json(self):
"""
根据jsonpath从json数据中提取数据并保存到对应的类属性中
:return:
"""
try:
# 将提取规则json数组转换成列表
rules = json.loads(self.case['extract'])
except Exception as e:
raise ValueError('用例【{}】的extract字段数据:{}格式不正确'.format(self.case['title'], self.case['extract']))
for rule in rules:
# 循环获取每个规则
name = rule[0] # 要绑定的属性名
exp = rule[1] # jsonpath 表达式
value = jsonpath(self.response.json(), exp) # 在响应数据中去提取数据
if value: # 注意:如果提取到了值是一个列表格式
# 如果提取到了数据就绑定到类属性中
setattr(self.__class__, name, value[0]) #
else:
raise ValueError('用例【{}】的提取表达式{}提取不到数据'.format(self.case['title'], self.case['extract']))

def __extract_data(self):
"""
从响应中提取数据并保存到对应的类属性中
:return:
"""
# 1. 判断是否需要提取
if self.case.get('extract'):
if self.case['res_type'].lower() == 'json':
self.__extract_data_from_json()
elif self.case['res_type'].lower() == 'html':
raise ValueError('还没有实现html数据的提取')
elif self.case['res_type'].lower() == 'xml':
raise ValueError('还没有实现xml数据的提取')
else:
raise ValueError('请填写正确的res_type')

def __assert_res(self):
"""
断言
:return:
"""
# 3. 断言
# 3.1 断言响应状态码
self.__assert_status_code()

# 3.2 响应结果断言
# 判断响应数据类型
self.assert_response()

# 3.3 数据库断言
self.assert_db()



testcases.xlsx

 

 

 

 

 

❀test_flow.py 方案一的self.step(case)位置打断言,看代码如何运行

 

 

 

 step into my code 先进入自己封装的函数

然后按第一个按钮运行封装好的函数

 

 

根据函数的运行步骤,同时验证一下数据

 

❀test_flow.py 方案二编写
import unittest
from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.test_data_handler import get_data_from_excel


@unittest.skip('我就不想让它执行')
class TestFlow(BaseCase):
name = '投资业务流'

def test_01register_normal_user(self):
"""
注册普通用户
:return:
"""
case = {
'title': '注册普通用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
'extract': '[["normal_mobile_phone", "$..mobile_phone"]]'
}
self.step(case)
# 将手机号码传递给下一个测试函数
# 可以用对象属性吗?不能
# 绑定在类属性上
self.__class__.normal_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_02login_normal_user(self):
"""
登录普通用户
"""
case = {
'title': '登录普通用户',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#normal_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定用户id,token属性
self.__class__.normal_member_id = self.response.json()['data']['id']
self.__class__.normal_token = self.response.json()['data']['token_info']['token']

def test_03add_loan(self):
"""
创建融资项目
:return:
"""
case = {
'title': '添加项目',
'url': 'add',
'method': 'post',
'request_data': '''
{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #normal_token#"},
"json":{
"member_id":#normal_member_id#,
"title":"实现财富自由",
"amount":5000,
"loan_rate":18.0,
"loan_term":6,
"loan_date_type":1,
"bidding_days":10}
}
''',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定属性 如果通过了,绑定loan_id
self.__class__.loan_id = self.response.json()['data']['id']

def test_04register_admin_user(self):
"""
注册管理员用户
:return:
"""
case = {
'title': '注册管理员用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678","type":0}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 保存依赖的数据 管理员手机号码
self.__class__.admin_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_05login_admin_user(self):
"""
登录管理员用户
:return:
"""
case = {
'title': '管理员用户登录',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#admin_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)
# 保存token
self.__class__.admin_token = self.response.json()['data']['token_info']['token']

def test_06audit_loan(self):
"""
审核项目
:return:
"""
case = {
'title': '审核项目',
'url': 'audit',
'method': 'patch',
'request_data': '''
{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #admin_token#"},
"json":{"loan_id":#loan_id#,"approved_or_not":true}
}
''',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'

}
self.step(case)

def test_07register_invest_user(self):
"""
注册投资用户
:return:
"""
case = {
'title': '注册投资用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 如果通过了就将电话号码保存到类属性中,共享给下一个用例
self.__class__.invest_phone = self.case['request_data']['json']['mobile_phone']

def test_08login_invest_user(self):
"""
登录投资用户
:return:
"""
case = {
'title': '普通投资用户登录',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#invest_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)
# 保存member_id,token
self.__class__.invest_member_id = self.response.json()['data']['id']
self.__class__.invest_token = self.response.json()['data']['token_info']['token']

def test_09invest_user_recharge(self):
"""
投资用户充值
:return:
"""
case = {
'title': '普通投资用户充值',
'url': 'recharge',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2",'
'"Authorization":"Bearer #invest_token#"},'
'"json": {"member_id":#invest_member_id#,"amount":5000}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)

def test_10invest(self):
"""
投资
:return:
"""
case = {
'title': '投资用户投资',
'url': 'invest',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2",'
'"Authorization":"Bearer #invest_token#"},'
'"json": {"member_id":#invest_member_id#,"loan_id":#loan_id#,"amount":5000}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)


# 方法二:

cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'invest_flow')


@ddt
class TestInvestFlow(BaseCase):

name = '投资业务流'

@list_data(cases)
def test_invest_flow(self, case):
self.step(case)




test_audit.py

from unittestreport import ddt, list_data

from common.base_case import BaseCase

from common.fixture import register, login, add_loan
from common.test_data_handler import (
generate_no_use_phone, get_data_from_excel)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'audit')


@ddt
class TestAudit(BaseCase):
name = '审核接口'

@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
# 1. 注册一个不同用户
mobile_phone = generate_no_use_phone()
pwd = '12345678'
register(mobile_phone, pwd)
# 2. 登录普通用户
data = login(mobile_phone, pwd)
# 3. 保存需要的数据
# 保存融资用户的member_id和token
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册管理员用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, pwd, _type=0)
# 5. 登录管理员用户
data = login(mobile_phone, pwd)
# 6. 保存需要的数据
# 保存管理员用户的token
cls.token = data['token_info']['token']

def setUp(self) -> None:
# 1. 创建一个项目
data = add_loan(self.normal_member_id, self.normal_token)
# 2. 保存新的项目的id
# 通过对象属性保存项目id
self.loan_id = data['id']

@list_data(cases)
def test_audit(self, case):
self.step(case)
test_recharge.py

from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.fixture import register, login
from common.test_data_handler import (
generate_no_use_phone,
get_data_from_excel,
)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'recharge')


@ddt
class TestRecharge(BaseCase):
name = '充值接口'

@classmethod
def setUpClass(cls) -> None:
# 执行以下父类的方法
super().setUpClass()
# 1. 注册一个用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, '12345678')

# 2. 登录
data = login(mobile_phone, '12345678')

# 3. 绑定数据
# 绑定用户的id,绑定token
# 绑定到类属性中
cls.member_id = data['id']
cls.token = data['token_info']['token']

@list_data(cases)
def test_recharge(self, case):
self.step(case)
test_register.py


from unittestreport import ddt, list_data

from common.test_data_handler import get_data_from_excel
from common.base_case import BaseCase

cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(BaseCase):
name = '注册接口'

@list_data(cases)
def test_register(self, case):

self.step(case)


if __name__ == '__main__':
import unittest
unittest.main()
main.py

import unittest

from unittestreport import TestRunner

import settings


if __name__ == '__main__':
# 1. 收集用例
ts = unittest.defaultTestLoader.discover(settings.TESTCASES_DIR)
# 2. 运行并生成测试报告

TestRunner(ts, **settings.REPORT_CONFIG).run()
settings.py

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
'invest': PROJECT_HOST + '/member/invest',
# =================== 项目模块 =========================
'add': PROJECT_HOST + '/loan/add',
'audit': PROJECT_HOST + '/loan/audit'
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'futureloan', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}



第 7 章. v3鉴权与多sql断言

一、V3版本权限验证

1.参数加密

如果在测试的过程中遇到了参数加密,有两种方案。

  1. 找开发协商,提供加密的模块,jar包。

  2. 找开发了解加密过程,自己用python代码封装

     

2.RSA加密

2.1 RSA加密的原理

数据论,需求两个大的素数比较简单,但将他们的乘积进行因式分解却极其困难。

将乘积作为加密秘钥。

RSA算法会生成一对秘钥,公钥和私钥。

公钥公开,用来加密数据,私钥不能泄露用来解密数据。也称为非对称加密。因为加密速度比较慢,一般用来做身份验证和短数据加密。

 

2.2 RSA库

安装
pip install rsa
使用
生成密钥对

 

⭐ 2048位加密更安全,但计算时候加密的效率越低

⭐生成的公钥和私钥是两个大整数

 

加密

 

解密

 

读取现有公钥

⭐一般有BEGIN PUBLIC KEY 和 END PUBLIC KEY的都是pem格式

 

3.封装V3版本token签名函数

  1. settings.py中添加公钥配置

settings.py

 

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 服务器公钥
SERVER_RSA_PUB_KEY = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQENQujkLfZfc5Tu9Z1LprzedE
O3F7gs+7bzrgPsMl29LX8UoPYvIG8C604CprBQ4FkfnJpnhWu2lvUB0WZyLq6sBr
tuPorOc42+gLnFfyhJAwdZB6SqWfDg7bW+jNe5Ki1DtU7z8uF6Gx+blEMGo8Dg+S
kKlZFc8Br7SHtbL2tQIDAQAB
-----END PUBLIC KEY-----
"""

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
'invest': PROJECT_HOST + '/member/invest',
# =================== 项目模块 =========================
'add': PROJECT_HOST + '/loan/add',
'audit': PROJECT_HOST + '/loan/audit'
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'future', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}


 

②封装ras加密获取sign签名函数

 

 

encrypt_handler.py

import base64
import time

import rsa


def rsa_encrypt(msg: str, server_pub_key: str):
"""
公钥加密
:param msg: 需要加密的信息
:param server_pub_key: pem格式的公钥
:return:
"""
# 1. 获取公钥对象
# 转换成字节码
pub_key_byte = server_pub_key.encode()
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(pub_key_byte)

# 2. 将加密数据转换成字节
content = msg.encode('utf-8')

# 3. 加密
crypt_msg = rsa.encrypt(content, pub_key)
# 4. 将加密的结果字节转化成base64编码的字符串
res = base64.b64encode(crypt_msg).decode()
return res


def generate_sign(token, pub_key: str):
"""
生成签名
:param token: token字符串
:param pub_key: pem格式的公钥
:return:
"""
# 1. 获取token的前50位
token_50 = token[:50]

# 2. 生成时间戳,取整数
timestamp = int(time.time())

# 3. 拼接token前50位和时间戳
msg = token_50 + str(timestamp)

# 4. RSA加密
sign = rsa_encrypt(msg, pub_key)

return sign, timestamp


if __name__ == '__main__':

import settings
from common.make_requests import send_http_request
from common.test_data_handler import generate_no_use_phone
from common.fixture import login, register
mobile_phone = generate_no_use_phone()
pwd = '12345678'
# 1. 注册
register(mobile_phone, pwd)
# 2. 登录
res = login(mobile_phone, pwd)
token = res['token_info']['token']
# 3. 充值
sign, timestamp = generate_sign(token, settings.SERVER_RSA_PUB_KEY)
data = {
'member_id': res['id'],
'amount': 50000,
'timestamp': timestamp,
'sign': sign
}
headers = {"X-Lemonban-Media-Type": "lemonban.v3", 'Authorization': 'Bearer {}'.format(token)}
res = send_http_request(settings.INTERFACES['recharge'], method='post', json=data, headers=headers)
print(res.json())


4.v3版本的投资接口测试

 

testcases.xlsx

 

 

 

 

 

 

 

 

 

 

 

 

 

invest.py

from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.test_data_handler import get_data_from_excel
from common.encrypt_handler import generate_sign

cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'invest')


@ddt
class TestInvest(BaseCase):
name = '投资接口'

@list_data(cases)
def test_invest(self, case):
self.step(case)

def send_http_request(self):
"""
复写父类方法实现v3版本鉴权
:return:
"""
if self.case['request_data']['headers']['X-Lemonban-Media-Type'] == 'lemonban.v3':
# 获取token,token此时在headers里面,发送请求前,token已经被替换到headers里面
token = self.case['request_data']['headers']['Authorization'].split(' ')[-1]
# 生成签名
sign, timestamp = generate_sign(token, self.settings.SERVER_RSA_PUB_KEY)
# 添加到参数中去
self.case['request_data']['json']['sign'] = sign
self.case['request_data']['json']['timestamp'] = timestamp

super().send_http_request()

 

二、多表校验,多条SQL校验

将多条sql用逗号分隔,处理的时候分割一下,逐条处理。

尽量写校验sql的时候,只需要查是否存在。

确实复杂的,重写sql校验的方法就可以了

总结:

  1. v3版本的鉴权,复写了send_request方法,实际项目中,根据实际鉴权逻辑,怎么方便怎么来

  2. 设计sql

 

SQL断言assert_db修改

base_case.py


import json
import unittest

from jsonpath import jsonpath

import settings
from common import db, logger
from common.make_requests import send_http_request
from common.test_data_handler import (
generate_no_use_phone,
replace_args_by_re)


class BaseCase(unittest.TestCase):
"""
测试基类
"""
# 将公用模块绑定到类属性上
db = db
logger = logger
settings = settings
name = 'base接口'

@classmethod
def setUpClass(cls) -> None:
cls.logger.info('========= {} 开始测试 ======='.format(cls.name))

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('========= {} 测试结束 ======='.format(cls.name))

def step(self, case):
"""
测试步骤
:param case:
:return:
"""

self.logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 绑定case到对象属性上便于其他对象方法访问
self.case = case
# 1. 处理测试数据
self.__process_test_data()
# 2. 发送请求
self.send_http_request()
# 3. 断言
self.__assert_res()
# 4. 提取依赖数据
self.__extract_data()
self.logger.info('********** 用例【{}】 测试成功 *************'.format(case['title']))

def generate_test_data(self):
"""
动态生成测试数据 phone
:return:
"""
# 子类可以复写实现自定义的生成测试数据
# 1 生成动态数据并替换
try:
if '$phone$' in self.case['request_data']:
# 如果request_data中存在生成数据的标志
phone = generate_no_use_phone()
# 立刻替换
self.case['request_data'] = self.case['request_data'].replace('$phone$', phone)
if self.case.get('sql'):
self.case['sql'] = self.case['sql'].replace('#phone#', phone)
except Exception as e:
self.logger.warning('用例【{}】生成测试数据$phone$的时候失败'.format(self.case['title']))
raise e

def __process_url(self):
"""
url的处理
:return:
"""
try:
if 'http' in self.case['url']:
# 全地址
pass
elif '/' in self.case['url']:
self.case['url'] = self.settings.PROJECT_HOST + self.case['url']
else:
self.case['url'] = self.settings.INTERFACES[self.case['url']]
except Exception as e:
self.logger.warning('用例【{}】在拼接处理url的时候失败'.format(self.case['title']))
raise e

def __replace_args(self):
"""
替换参数
:return:
"""
try:
self.case['url'] = replace_args_by_re(self.case['url'], self)
self.case['request_data'] = replace_args_by_re(self.case['request_data'], self)
# sql也需要替换
if self.case.get('sql'):
self.case['sql'] = replace_args_by_re(self.case['sql'], self)
except Exception as e:
self.logger.warning('用例【{}】替换参数时失败'.format(self.case['title']))
raise e

def __process_test_data(self):
"""
处理测试数据
:return:
"""

# 1. 生成测试数据
self.generate_test_data()
# 2. url处理
self.__process_url()
# 3. 替换依赖参数
self.__replace_args()

# 4. 将请求数据和期望数据转换为python对象
try:
self.case['request_data'] = json.loads(self.case['request_data'])
self.case['expect_data'] = json.loads(self.case['expect_data'])
except Exception as e:
self.logger.warning('用例【{}】在请求参数反序列化时失败'.format(self.case['title']))
raise e

def send_http_request(self):
"""
发送http请求
:return:
"""
# 2. 发送请求
# 根据测试用例数据来发送请求
try:
self.response = send_http_request(
self.case['url'],
self.case['method'],
**self.case['request_data']
)
except Exception as e:
self.logger.warning('用例【{}】在发送请求时失败'.format(self.case['title']))
raise e

def __assert_status_code(self):
"""
响应状态码断言
:return:
"""
try:
self.assertEqual(self.case['status_code'], self.response.status_code)
except AssertionError as e:
self.logger.warning('用例【{}】响应状态码断言失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】响应状态码断言成功'.format(self.case['title']))

def assert_response(self):
"""
断言响应数据
:return:
"""
if self.case.get('res_type') == 'json':
res = self.response.json()
elif self.case.get('res_type') == 'xml':
pass
elif self.case.get('res_type') == 'html':
pass

try:
self.assertEqual(self.case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
self.logger.warning('用例【{}】响应数据断言失败'.format(self.case['title']))
self.logger.info('用例【{}】期望的结果是{}'.format(self.case['title'], self.case['expect_data']))
self.logger.info('用例【{}】返回数据是{}'.format(self.case['title'], res))
raise e
else:
self.logger.info('用例【{}】响应数据断言成功'.format(self.case['title']))

def assert_db(self):
"""
数据库断言
:return:
"""
if self.case.get('sql'):
sqls = self.case['sql'].split(',')
for sql in sqls:
self.logger.info('用例【{}】数据校验的sql为:{}'.format(self.case['title'], sql))
try:
self.assertTrue(self.db.exist(sql))
except AssertionError as e:
self.logger.warning('用例【{}】数据库断言失败'.format(self.case['title']))
raise e
except Exception as e:
self.logger.warning('用例【{}】数据库查询失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】数据断言成功'.format(self.case['title']))

def __extract_data_from_json(self):
"""
根据jsonpath从json数据中提取数据并保存到对应的类属性中
:return:
"""
try:
# 将提取规则json数组转换成列表
rules = json.loads(self.case['extract'])
except Exception as e:
raise ValueError('用例【{}】的extract字段数据:{}格式不正确'.format(self.case['title'], self.case['extract']))
for rule in rules:
# 循环获取每个规则
name = rule[0] # 要绑定的属性名
exp = rule[1] # jsonpath 表达式
value = jsonpath(self.response.json(), exp) # 在响应数据中去提取数据
if value: # 注意:如果提取到了值是一个列表格式
# 如果提取到了数据就绑定到类属性中
setattr(self.__class__, name, value[0]) #
else:
raise ValueError('用例【{}】的提取表达式{}提取不到数据'.format(self.case['title'], self.case['extract']))

def __extract_data(self):
"""
从响应中提取数据并保存到对应的类属性中
:return:
"""
# 1. 判断是否需要提取
if self.case.get('extract'):
if self.case['res_type'].lower() == 'json':
self.__extract_data_from_json()
elif self.case['res_type'].lower() == 'html':
raise ValueError('还没有实现html数据的提取')
elif self.case['res_type'].lower() == 'xml':
raise ValueError('还没有实现xml数据的提取')
else:
raise ValueError('请填写正确的res_type')

def __assert_res(self):
"""
断言
:return:
"""
# 3. 断言
# 3.1 断言响应状态码
self.__assert_status_code()

# 3.2 响应结果断言
# 判断响应数据类型
self.assert_response()

# 3.3 数据库断言
self.assert_db()


第 8 章. 极版接口测试框架

一、session鉴权的处理

1.requests的会话对象

会话对象可以跨请求保持某些参数。在同一个session实例发出的所有请求之间保持cookie.

 

 

 

 

 

 xxx

 

 

 

 

 

 

2.base中封装session鉴权的http请求函数

封装思路:

  • 只需要在一个会话中使用同一个会话对象即可。

  • 给每个测试用例类创建一个会话对象。

 

 

3.课堂派测试案例

testcase

test_coures_flow.py
import unittest

from unittestreport import ddt, list_data

from common.base_case import BaseCase

# 两条测试用例
cases = [
{'title': '课堂派登录',
'method': 'post',
'url': 'https://v4.ketangpai.com/UserApi/login',
'request_data': '{"data": {"email": "877649301@qq.com", "password": "Pythonxinlan", "remember": 0}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"status": 1}'
},
{'title': '获取所有课程信息',
'method': 'get',
'url': 'https://v4.ketangpai.com/CourseApi/lists',
'request_data': '{}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"status": 1}'
},
]


@ddt
class TestCourseFlow(BaseCase):
name = '课堂派业务流'

@list_data(cases)
def test_course(self, case):
self.step(case)

def assert_response(self):
"""
复写断言响应数据这个方法,因为这个expect_data不一样
:return:
"""
if self.case.get('res_type') == 'json':
res = self.response.json()
elif self.case.get('res_type') == 'xml':
pass
elif self.case.get('res_type') == 'html':
pass

try:
self.assertEqual(self.case['expect_data'], {'status': res['status']})
except AssertionError as e:
self.logger.warning('用例【{}】响应数据断言失败'.format(self.case['title']))
self.logger.info('用例【{}】期望的结果是{}'.format(self.case['title'], self.case['expect_data']))
self.logger.info('用例【{}】返回数据是{}'.format(self.case['title'], res))
raise e
else:
self.logger.info('用例【{}】响应数据断言成功'.format(self.case['title']))


if __name__ == '__main__':
unittest.main()

二、mock测试

1.简述

挡板

mock测试就是在测试过程中,对于某些不容易构成或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。

典型应用场景:

  1. 当某个单元测试依赖另外一个函数,而这个函数还未开发完成。那么就可以使用这个函数的mock对象来完成测试。

  2. 当某个接口依赖另外一个接口,而这个接口未开发完成,或者不方便调用(例如第三方的支付接口),那么可以使用mock服务模拟这个依赖接口来完成。

     

     

     

2.unittest.mock

 ⭐mock_function 就相当于some_function函数,传参的类型和数量必须一致,否则会报错

 

 

3.实现充值接口的mock测试

test_recharge.py

from unittest.mock import MagicMock

from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.fixture import register, login
from common.test_data_handler import (
generate_no_use_phone,
get_data_from_excel,
)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'recharge')


@ddt
class TestRecharge(BaseCase):
name = '充值接口'

@classmethod
def setUpClass(cls) -> None:
# 执行以下父类的方法
super().setUpClass()
# 1. 注册一个用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, '12345678')

# 2. 登录
data = login(mobile_phone, '12345678')

# 3. 绑定数据
# 绑定用户的id,绑定token
# 绑定到类属性中
cls.member_id = data['id']
cls.token = data['token_info']['token']

@list_data(cases)
def test_recharge(self, case):
self.step(case)

def send_http_request(self):
# 假设第三方支付接口的地址是:
alipay_url = 'http://www.fastmock.site/mock/888'
# 创建一个mock函数
alipay_mock = MagicMock(return_value={"code":0,"msg":"支付成功"})
# 执行mock函数
pay_res = alipay_mock(alipay_url,method='POST')
if pay_res['code']==0:
raise RuntimeError('支付宝支付不成功!')
super().send_http_request()

4.mock服务

mock服务就是一个web应用程序,可以自定义接口地址,发送参数,以及响应结果,实现技术简单。

例:faskmock网站

🖊编辑接口

 配置好后,用postman跑通

 ⭐也可以用python写一个mock服务

⭐下为mock网站实现mock充值

并修改充值接口的测试用例

 

request_data数据可以随便填

 

 

⭐改进测试用例,第一条用例应该要能加一个提取数据,并将提取数据传给第二条用例

断言成功后就提取,把数据保存到类属性上,前置条件成功后面测试才有意义

 

⭐只在业务流中,对正例有效,所以第一条用力应该写在业务流里面不写在充值测试用例里面

 

 

三、接口自动化框架终极代码

①common

__init__.py

# python中的每一个包下都会有一个__init__.py文件

# 当包被导入的时候,这个__init__.py文件里的代码会被执行,并且只会执行一遍
import unittest
import settings
from common.log_handler import get_logger
from common.db_handler import SQLdbHandler

logger = get_logger(**settings.LOG_CONFIG)
db = SQLdbHandler(settings.DB_CONFIG)

base_case.py

import json
import unittest

import requests
from jsonpath import jsonpath

import settings
from common import db, logger
from common.make_requests import send_http_request
from common.test_data_handler import (
generate_no_use_phone,
replace_args_by_re)


class BaseCase(unittest.TestCase):
"""
测试基类
"""
# 将公用模块绑定到类属性上
db = db
logger = logger
settings = settings
name = 'base接口'
session = requests.session()

@classmethod
def setUpClass(cls) -> None:
cls.logger.info('========= {} 开始测试 ======='.format(cls.name))

@classmethod
def tearDownClass(cls) -> None:
cls.logger.info('========= {} 测试结束 ======='.format(cls.name))

def step(self, case):
"""
测试步骤
:param case:
:return:
"""

self.logger.info('********** 用例【{}】 开始执行 *************'.format(case['title']))
# 绑定case到对象属性上便于其他对象方法访问
self.case = case
# 1. 处理测试数据
self.__process_test_data()
# 2. 发送请求
self.send_http_request()
# 3. 断言
self.__assert_res()
# 4. 提取依赖数据
self.__extract_data()
self.logger.info('********** 用例【{}】 测试成功 *************'.format(case['title']))

def generate_test_data(self):
"""
动态生成测试数据 phone
:return:
"""
# 子类可以复写实现自定义的生成测试数据
# 1 生成动态数据并替换
try:
if '$phone$' in self.case['request_data']:
# 如果request_data中存在生成数据的标志
phone = generate_no_use_phone()
# 立刻替换
self.case['request_data'] = self.case['request_data'].replace('$phone$', phone)
if self.case.get('sql'):
self.case['sql'] = self.case['sql'].replace('#phone#', phone)
except Exception as e:
self.logger.warning('用例【{}】生成测试数据$phone$的时候失败'.format(self.case['title']))
raise e

def __process_url(self):
"""
url的处理
:return:
"""
try:
if 'http' in self.case['url']:
# 全地址
pass
elif '/' in self.case['url']:
self.case['url'] = self.settings.PROJECT_HOST + self.case['url']
else:
self.case['url'] = self.settings.INTERFACES[self.case['url']]
except Exception as e:
self.logger.warning('用例【{}】在拼接处理url的时候失败'.format(self.case['title']))
raise e

def __replace_args(self):
"""
替换参数
:return:
"""
try:
self.case['url'] = replace_args_by_re(self.case['url'], self)
self.case['request_data'] = replace_args_by_re(self.case['request_data'], self)
# sql也需要替换
if self.case.get('sql'):
self.case['sql'] = replace_args_by_re(self.case['sql'], self)
except Exception as e:
self.logger.warning('用例【{}】替换参数时失败'.format(self.case['title']))
raise e

def __process_test_data(self):
"""
处理测试数据
:return:
"""

# 1. 生成测试数据
self.generate_test_data()
# 2. url处理
self.__process_url()
# 3. 替换依赖参数
self.__replace_args()

# 4. 将请求数据和期望数据转换为python对象
try:
self.case['request_data'] = json.loads(self.case['request_data'])
self.case['expect_data'] = json.loads(self.case['expect_data'])
except Exception as e:
self.logger.warning('用例【{}】在请求参数反序列化时失败'.format(self.case['title']))
raise e

def send_http_request(self):
"""
发送http请求
:return:
"""
# 2. 发送请求
# 根据测试用例数据来发送请求
try:
self.response = self.send_http_request_by_session(
self.case['url'],
self.case['method'],
**self.case['request_data']
)
except Exception as e:
self.logger.warning('用例【{}】在发送请求时失败'.format(self.case['title']))
raise e

def send_http_request_by_session(self, url, method, **kwargs) -> requests.Response:
"""
发送http请求自动处理cookie信息
:param url:
:param method:
:param kwargs:
:return:
"""
method = method.lower()
return getattr(self.session, method)(url=url, **kwargs)

def __assert_status_code(self):
"""
响应状态码断言
:return:
"""
try:
self.assertEqual(self.case['status_code'], self.response.status_code)
except AssertionError as e:
self.logger.warning('用例【{}】响应状态码断言失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】响应状态码断言成功'.format(self.case['title']))

def assert_response(self):
"""
断言响应数据
:return:
"""
if self.case.get('res_type') == 'json':
res = self.response.json()
elif self.case.get('res_type') == 'xml':
pass
elif self.case.get('res_type') == 'html':
pass

try:
self.assertEqual(self.case['expect_data'], {'code': res['code'], 'msg': res['msg']})
except AssertionError as e:
self.logger.warning('用例【{}】响应数据断言失败'.format(self.case['title']))
self.logger.info('用例【{}】期望的结果是{}'.format(self.case['title'], self.case['expect_data']))
self.logger.info('用例【{}】返回数据是{}'.format(self.case['title'], res))
raise e
else:
self.logger.info('用例【{}】响应数据断言成功'.format(self.case['title']))

def assert_db(self):
"""
数据库断言
:return:
"""
if self.case.get('sql'):
sqls = self.case['sql'].split(',')
for sql in sqls:
self.logger.info('用例【{}】数据校验的sql为:{}'.format(self.case['title'], sql))
try:
self.assertTrue(self.db.exist(sql))
except AssertionError as e:
self.logger.warning('用例【{}】数据库断言失败'.format(self.case['title']))
raise e
except Exception as e:
self.logger.warning('用例【{}】数据库查询失败'.format(self.case['title']))
raise e
else:
self.logger.info('用例【{}】数据断言成功'.format(self.case['title']))

def __extract_data_from_json(self):
"""
根据jsonpath从json数据中提取数据并保存到对应的类属性中
:return:
"""
try:
# 将提取规则json数组转换成列表
rules = json.loads(self.case['extract'])
except Exception as e:
raise ValueError('用例【{}】的extract字段数据:{}格式不正确'.format(self.case['title'], self.case['extract']))
for rule in rules:
# 循环获取每个规则
name = rule[0] # 要绑定的属性名
exp = rule[1] # jsonpath 表达式
value = jsonpath(self.response.json(), exp) # 在响应数据中去提取数据
if value: # 注意:如果提取到了值是一个列表格式
# 如果提取到了数据就绑定到类属性中
setattr(self.__class__, name, value[0]) #
else:
raise ValueError('用例【{}】的提取表达式{}提取不到数据'.format(self.case['title'], self.case['extract']))

def __extract_data(self):
"""
从响应中提取数据并保存到对应的类属性中
:return:
"""
# 1. 判断是否需要提取
if self.case.get('extract'):
if self.case['res_type'].lower() == 'json':
self.__extract_data_from_json()
elif self.case['res_type'].lower() == 'html':
raise ValueError('还没有实现html数据的提取')
elif self.case['res_type'].lower() == 'xml':
raise ValueError('还没有实现xml数据的提取')
else:
raise ValueError('请填写正确的res_type')

def __assert_res(self):
"""
断言
:return:
"""
# 3. 断言
# 3.1 断言响应状态码
self.__assert_status_code()

# 3.2 响应结果断言
# 判断响应数据类型
self.assert_response()

# 3.3 数据库断言
self.assert_db()

db_handler.py

import pymysql
from pymysql.cursors import DictCursor


class SQLdbHandler:
"""
sql数据库查询类
"""
def __init__(self, db_config):
# 创建连接
# 根据不同的数据库,创建不同的链接
engine = db_config.pop('engine', 'mysql')
if engine.lower() == 'mysql':
self.conn = pymysql.connect(**db_config)

def get_one(self, sql, res_type='t'):
"""
获取一条数据
:param sql:
:param res_type: 返回数据的类型 默认为't表示以元组的形式返回
'd'表示以字典的形式返回
:return: 元组/字典
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchone()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchone()

def get_many(self, sql, size, res_type='t'):
"""
获取多条数据
:param sql:
:param size: 指定的条数
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchmany(size)

def get_all(self, sql, res_type='t'):
"""
获取所有数据
:param sql:
:param res_type:
:return:
"""
if res_type == 't':
with self.conn.cursor() as cursor:
cursor.execute(sql)
return cursor.fetchall()
else:
with self.conn.cursor(DictCursor) as cursor:
cursor.execute(sql)
return cursor.fetchall()

def exist(self, sql):
"""
查询数据是否存在
:param sql:
:return:
"""
if self.get_one(sql):
return True
else:
return False

def __del__(self):
"""
对象销毁的时候自动被调用
:return:
"""
self.conn.close()


if __name__ == '__main__':
import settings
db = SQLdbHandler(settings.DB_CONFIG)
sql = 'select leave_amount from member where id=10000000000'
res = db.get_one(sql, 'd')
print(res)
print(db.exist(sql))

encrypt_handler.py

import base64
import time

import rsa


def rsa_encrypt(msg: str, server_pub_key: str):
"""
公钥加密
:param msg: 需要加密的信息
:param server_pub_key: pem格式的公钥
:return:
"""
# 1. 获取公钥对象
# 转换成字节码
pub_key_byte = server_pub_key.encode()
pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(pub_key_byte)

# 2. 将加密数据转换成字节
content = msg.encode('utf-8')

# 3. 加密
crypt_msg = rsa.encrypt(content, pub_key)
# 4. 将加密的结果字节转化成base64编码的字符串
res = base64.b64encode(crypt_msg).decode()
return res


def generate_sign(token, pub_key: str):
"""
生成签名
:param token: token字符串
:param pub_key: pem格式的公钥
:return:
"""
# 1. 获取token的前50位
token_50 = token[:50]

# 2. 生成时间戳
timestamp = int(time.time())

# 3. 拼接token前50位和时间戳
msg = token_50 + str(timestamp)

# 4. RSA加密
sign = rsa_encrypt(msg, pub_key)

return sign, timestamp


if __name__ == '__main__':

import settings
from common.make_requests import send_http_request
from common.test_data_handler import generate_no_use_phone
from common.fixture import login, register
mobile_phone = generate_no_use_phone()
pwd = '12345678'
# 1. 注册
register(mobile_phone, pwd)
# 2. 登录
res = login(mobile_phone, pwd)
token = res['token_info']['token']
# 3. 充值
sign, timestamp = generate_sign(token, settings.SERVER_RSA_PUB_KEY)
data = {
'member_id': res['id'],
'amount': 50000,
'timestamp': timestamp,
'sign': sign
}
headers = {"X-Lemonban-Media-Type": "lemonban.v3", 'Authorization': 'Bearer {}'.format(token)}
res = send_http_request(settings.INTERFACES['recharge'], method='post', json=data, headers=headers)
print(res.json())

fixture.py

 

"""
一些脚手架代码
"""
import requests

import settings
from common import logger


def register(mobile_phone, pwd, reg_name=None, _type=None):
"""
注册用户
:param mobile_phone:
:param pwd:
:param reg_name:
:param _type:
:return:
"""
# 1. 构造发送注册请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
if reg_name:
data['reg_name'] = reg_name
# 因为会传入0进来,所有不能直接if _type
if _type is not None:
data['type'] = _type

headers = {"X-Lemonban-Media-Type": "lemonban.v1"}
url = settings.INTERFACES['register']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('注册用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('注册用户失败')
raise e


def login(mobile_phone, pwd):
# 1. 构造发送登录请求的参数
data = {
'mobile_phone': mobile_phone,
'pwd': pwd
}
# v2才会返回token
headers = {"X-Lemonban-Media-Type": "lemonban.v2"}
url = settings.INTERFACES['login']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('登录用户成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


def add_loan(member_id, token, title='借钱实现财富自由', amount=5000, loan_rate=12.0,
loan_term=3, loan_date_type=1, bidding_days=5):
"""
添加一个项目
:param member_id:
:param token:
:param title:
:param amount:
:param loan_rate:
:param loan_term:
:param loan_date_type:
:param bidding_days:
:return:
"""
# 1. 构造发送登录请求的参数
data = {
'member_id': member_id,
'title': title,
'amount': amount,
'loan_rate': loan_rate,
'loan_term': loan_term,
'loan_date_type': loan_date_type,
'bidding_days': bidding_days
}
# 需要鉴权
headers = {"X-Lemonban-Media-Type": "lemonban.v2", 'Authorization': 'Bearer {}'.format(token)}
url = settings.INTERFACES['add']
try:
res = requests.post(url=url, json=data, headers=headers)
if res.status_code == 200:
if res.json()['code'] == 0:
logger.info('创建项目成功')
return res.json()['data']
raise ValueError(res.json())
except Exception as e:
logger.warning('登录用户失败')
raise e


if __name__ == '__main__':
from common.test_data_handler import generate_no_use_phone
phone = generate_no_use_phone()
register(phone, '12345678', _type=0)
res = login(phone, '12345678')
token = res['token_info']['token']
member_id = res['id']
loan = add_loan(member_id, token)
print(loan)

log_handler.py

import logging
from logging.handlers import TimedRotatingFileHandler


def get_logger(name, filename, encoding='utf-8', fmt=None, when='d', interval=1, backup_count=7, debug=False):
"""

:param name: 日志器的名字
:param filename: 日志文件名(包含路径)
:param encoding: 字符编码
:param fmt: 日志格式
:param when: 日志轮转时间单位
:param interval: 间隔
:param backup_count: 日志文件个数
:param debug: 调试模式
:return:
"""
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
# 文件处理器的等级一般情况一定比控制台要高
if debug:
file_level = logging.DEBUG
console_level = logging.DEBUG
else:
file_level = logging.WARNING
console_level = logging.INFO

if fmt is None:
fmt = '%(levelname)s %(asctime)s [%(filename)s-->line:%(lineno)d]:%(message)s'

file_handler = TimedRotatingFileHandler(
filename=filename, when=when, interval=interval, backupCount=backup_count, encoding=encoding)
file_handler.setLevel(file_level)

console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

logger.addHandler(file_handler)
logger.addHandler(console_handler)

return logger


if __name__ == '__main__':
import settings
log = get_logger(**settings.LOG_CONFIG)
log.info('我是普通信息')
log.warning('我是警告信息')

make_requests.py

不用,已在base里封装为session鉴权

test_data_handler.py

import json
import re
import random

from jsonpath import jsonpath

from openpyxl import load_workbook
from common import db


def get_data_from_excel(file, sheet_name=None):
"""
获取Excel文件中的测试数据
"""
# 1. 读取excel文件
wb = load_workbook(file)

# 2. 读取对应的表
if sheet_name is None:
ws = wb.active
else:
ws = wb[sheet_name]
# 3. 创建一个列表容器存放数据
data = []
# 4. 获取表头头
row_list = list(ws.rows)
title = [item.value for item in row_list[0]]
# 5. 获取其他数据
for row in row_list[1:]:
# 获取每一个行数据
temp = [i.value for i in row]
# 将表头与这一行数据打包,转换成字典
data.append(dict(zip(title, temp)))

return data


def generate_phone():
"""
随机的生成手机号码
:return:
"""
# 1开头
# 11位
# 第二个数字是3-9

# phone = ['1', str(random.randint(3, 9))]
phone = ['158']
for i in range(8):
phone.append(str(random.randint(0, 9)))
return ''.join(phone)


def generate_no_use_phone(sql="select id from member where mobile_phone='{}'"):
"""
生成没有注册的手机号码
:return:
"""
while True:
phone = generate_phone()

if not db.exist(sql.format(phone)):
return phone


def replace_args_by_re(s, obj):
"""
通过正则表达式动态替换参数
:param s: 需要被替换的参数字符串
:param obj: 提供数据的对象
:return:
"""
# 1. 先找出字符串中的槽位
args = re.findall('#(.*?)#', s)
# 2. 循环参数
for arg in args:
# 3. 获取obj对应参数名的属性
value = getattr(obj, arg, None)
# 4. 替换
if value:
s = s.replace('#{}#'.format(arg), str(value))
return s


def extract_data(rule, json_data, obj):
"""
根据提取表达式提取响应数据中的值并绑定到obj的对应属性上
:param rule: 提取规则
:param json_data:
:param obj:
:return:
"""
data = json.loads(json_data)

name = rule[0] # 要绑定的属性名
exp = rule[1] # jsonpath 表达式
value = jsonpath(data, exp) # 在响应数据中去提取数据
if value:
# 如果提取到了数据就绑定到类属性中
setattr(obj, name, value[0])
else:
raise ValueError('用例的提取表达式{}提取不到数据'.format(rule))


if __name__ == '__main__':
rule = ["normal_mobile_phone", "$..mobile_phone"]

class A:
pass

data = '{"mobile_phone": "15873061798","pwd": "123456"}'

extract_data(rule, data, A)
print(A.normal_mobile_phone)
# s = '''{
# "headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #aaa#"},
# "json":{"loan_id":#bbb#,"approved_or_not":true}
# }'''
# class A:
# pass
# A.token = '我是token'
# A.loan_id = 8888
# res = replace_args_by_re(s, A)
# print(res)
# res = get_data_from_excel(r'D:\project\classes\py41\day23\test_data\testcases.xlsx', 'register')
# print(res)
# print(generate_phone())
# print(generate_no_use_phone())

②logs

③reports

④test_data

testcases.xlsx

testcases.xlsx

⑤testcases

test_audit.py

from unittestreport import ddt, list_data

from common.base_case import BaseCase

from common.fixture import register, login, add_loan
from common.test_data_handler import (
generate_no_use_phone, get_data_from_excel)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'audit')


@ddt
class TestAudit(BaseCase):
name = '审核接口'

@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
# 1. 注册一个不同用户
mobile_phone = generate_no_use_phone()
pwd = '12345678'
register(mobile_phone, pwd)
# 2. 登录普通用户
data = login(mobile_phone, pwd)
# 3. 保存需要的数据
# 保存融资用户的member_id和token
cls.normal_member_id = data['id']
cls.normal_token = data['token_info']['token']
# 4. 注册管理员用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, pwd, _type=0)
# 5. 登录管理员用户
data = login(mobile_phone, pwd)
# 6. 保存需要的数据
# 保存管理员用户的token
cls.token = data['token_info']['token']

def setUp(self) -> None:
# 1. 创建一个项目
data = add_loan(self.normal_member_id, self.normal_token)
# 2. 保存新的项目的id
# 通过对象属性保存项目id
self.loan_id = data['id']

@list_data(cases)
def test_audit(self, case):
self.step(case)

test_flow.py

import unittest
from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.test_data_handler import get_data_from_excel


@unittest.skip('我就不想让它执行')
class TestFlow(BaseCase):
name = '投资业务流'

def test_01register_normal_user(self):
"""
注册普通用户
:return:
"""
case = {
'title': '注册普通用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
'extract': '[["normal_mobile_phone", "$..mobile_phone"]]'
}
self.step(case)
# 将手机号码传递给下一个测试函数
# 可以用对象属性吗?不能
# 因为每一个单元测试函数都会单独实例化一个用例,两个用例之间是单独的个体,A用例定义的属性,B用例不能访问,但是两个用例有共同的类爸爸
# 绑定在类属性上
self.__class__.normal_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_02login_normal_user(self):
"""
登录普通用户
"""
case = {
'title': '登录普通用户',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#normal_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定用户id,token属性
self.__class__.normal_member_id = self.response.json()['data']['id']
self.__class__.normal_token = self.response.json()['data']['token_info']['token']

def test_03add_loan(self):
"""
创建融资项目
:return:
"""
case = {
'title': '添加项目',
'url': 'add',
'method': 'post',
'request_data': '''
{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #normal_token#"},
"json":{
"member_id":#normal_member_id#,
"title":"实现财富自由",
"amount":5000,
"loan_rate":18.0,
"loan_term":6,
"loan_date_type":1,
"bidding_days":10}
}
''',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 绑定属性 如果通过了,绑定loan_id
self.__class__.loan_id = self.response.json()['data']['id']

def test_04register_admin_user(self):
"""
注册管理员用户
:return:
"""
case = {
'title': '注册管理员用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678","type":0}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 保存依赖的数据 管理员手机号码
self.__class__.admin_mobile_phone = self.case['request_data']['json']['mobile_phone']

def test_05login_admin_user(self):
"""
登录管理员用户
:return:
"""
case = {
'title': '管理员用户登录',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#admin_mobile_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)
# 保存token
self.__class__.admin_token = self.response.json()['data']['token_info']['token']

def test_06audit_loan(self):
"""
审核项目
:return:
"""
case = {
'title': '审核项目',
'url': 'audit',
'method': 'patch',
'request_data': '''
{
"headers": {"X-Lemonban-Media-Type": "lemonban.v2","Authorization":"Bearer #admin_token#"},
"json":{"loan_id":#loan_id#,"approved_or_not":true}
}
''',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'

}
self.step(case)

def test_07register_invest_user(self):
"""
注册投资用户
:return:
"""
case = {
'title': '注册投资用户',
'url': 'register',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v1"},'
'"json": {"mobile_phone":$phone$,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}'
}
self.step(case)
# 如果通过了就将电话号码保存到类属性中,共享给下一个用例
self.__class__.invest_phone = self.case['request_data']['json']['mobile_phone']

def test_08login_invest_user(self):
"""
登录投资用户
:return:
"""
case = {
'title': '普通投资用户登录',
'url': 'login',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2"},'
'"json": {"mobile_phone":#invest_phone#,"pwd":"12345678"}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)
# 保存member_id,token
self.__class__.invest_member_id = self.response.json()['data']['id']
self.__class__.invest_token = self.response.json()['data']['token_info']['token']

def test_09invest_user_recharge(self):
"""
投资用户充值
:return:
"""
case = {
'title': '普通投资用户充值',
'url': 'recharge',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2",'
'"Authorization":"Bearer #invest_token#"},'
'"json": {"member_id":#invest_member_id#,"amount":5000}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)

def test_10invest(self):
"""
投资
:return:
"""
case = {
'title': '投资用户投资',
'url': 'invest',
'method': 'post',
'request_data': '{"headers": {"X-Lemonban-Media-Type": "lemonban.v2",'
'"Authorization":"Bearer #invest_token#"},'
'"json": {"member_id":#invest_member_id#,"loan_id":#loan_id#,"amount":5000}}',
'status_code': 200,
'res_type': 'json',
'expect_data': '{"code":0,"msg":"OK"}',
}
self.step(case)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'invest_flow')


@ddt
class TestInvestFlow(BaseCase):

name = '投资业务流'

@list_data(cases)
def test_invest_flow(self, case):
self.step(case)

test_invest.py

from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.test_data_handler import get_data_from_excel
from common.encrypt_handler import generate_sign

cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'invest')


@ddt
class TestInvest(BaseCase):
name = '投资接口'

@list_data(cases)
def test_invest(self, case):
self.step(case)

def send_http_request(self):
"""
复写父类方法实现v3版本鉴权
:return:
"""
if self.case['request_data']['headers']['X-Lemonban-Media-Type'] == 'lemonban.v3':
# 获取token
token = self.case['request_data']['headers']['Authorization'].split(' ')[-1]
# 生成签名
sign, timestamp = generate_sign(token, self.settings.SERVER_RSA_PUB_KEY)
# 添加到参数中去
self.case['request_data']['json']['sign'] = sign
self.case['request_data']['json']['timestamp'] = timestamp

super().send_http_request()

test_recharge.py

from unittest.mock import MagicMock

from unittestreport import ddt, list_data

from common.base_case import BaseCase
from common.fixture import register, login
from common.test_data_handler import (
generate_no_use_phone,
get_data_from_excel,
)


cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'recharge')


@ddt
class TestRecharge(BaseCase):
name = '充值接口'

@classmethod
def setUpClass(cls) -> None:
# 执行以下父类的方法
super().setUpClass()
# 1. 注册一个用户
mobile_phone = generate_no_use_phone()
register(mobile_phone, '12345678')

# 2. 登录
data = login(mobile_phone, '12345678')

# 3. 绑定数据
# 绑定用户的id,绑定token
# 绑定到类属性中
cls.member_id = data['id']
cls.token = data['token_info']['token']

@list_data(cases)
def test_recharge(self, case):
self.step(case)

test_register.py

from unittestreport import ddt, list_data

from common.test_data_handler import get_data_from_excel
from common.base_case import BaseCase

cases = get_data_from_excel(BaseCase.settings.TEST_DATA_FILE, 'register')


@ddt
class RegisterTestCase(BaseCase):
name = '注册接口'

@list_data(cases)
def test_register(self, case):

self.step(case)


if __name__ == '__main__':
import unittest
unittest.main()

testcases~test_coures_flow.py session鉴权测试课堂派

⑥settings.py

import os

# 项目根目录
BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# 项目主机
PROJECT_HOST = 'http://api.lemonban.com/futureloan'

# 服务器公钥
SERVER_RSA_PUB_KEY = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDQENQujkLfZfc5Tu9Z1LprzedE
O3F7gs+7bzrgPsMl29LX8UoPYvIG8C604CprBQ4FkfnJpnhWu2lvUB0WZyLq6sBr
tuPorOc42+gLnFfyhJAwdZB6SqWfDg7bW+jNe5Ki1DtU7z8uF6Gx+blEMGo8Dg+S
kKlZFc8Br7SHtbL2tQIDAQAB
-----END PUBLIC KEY-----
"""

# 接口地址
INTERFACES = {
# =================== 用户模块 =========================
'register': PROJECT_HOST + '/member/register',
'login': PROJECT_HOST + '/member/login',
'recharge': PROJECT_HOST + '/member/recharge',
'invest': PROJECT_HOST + '/member/invest',
# =================== 项目模块 =========================
'add': PROJECT_HOST + '/loan/add',
'audit': PROJECT_HOST + '/loan/audit'
}



# 日志配置
LOG_CONFIG = {
'name': 'py41',
'filename': os.path.join(BASE_DIR, 'logs', 'py41.log'),
# 'encoding': 'utf-8',
# 'fmt': None,
# 'when': 'd',
# 'interval': 1,
# 'backup_count': 7,
'debug': True
}

# 测试数据
TEST_DATA_FILE = os.path.join(BASE_DIR, 'test_data', 'testcases.xlsx')

# 测试用例目录
TESTCASES_DIR = os.path.join(BASE_DIR, 'testcases')

# 测试报告
REPORT_CONFIG = {
'filename': 'py41接口自动化测试报告.html',
'report_dir': os.path.join(BASE_DIR, 'reports'),
'title': 'py41接口自动化测试报告',
'tester': '心蓝',
'desc': "借钱是快靠不住的",
# templates=1
}

# 数据库配置
DB_CONFIG = {
'engine': 'mysql', # 指定数据库引擎,
'host': 'api.lemonban.com', # 主机
'user': 'future', # 用户名
'password': '123456', # 密码
'db': 'futureloan', # 数据库
'port': 3306, # 端口
'charset': 'utf8', # 字符串编码
'autocommit': True
}

⑦main.py

import unittest

from unittestreport import TestRunner

import settings


if __name__ == '__main__':
# 1. 收集用例
ts = unittest.defaultTestLoader.discover(settings.TESTCASES_DIR)
# 2. 运行并生成测试报告

TestRunner(ts, **settings.REPORT_CONFIG).run()

 

 

四、接口自动化总结

1.接口框架梳理

1)unittest单元测试框架

 

 

2)原生的文本报告不好看怎么办?

 

 

3)怎么解耦测试用例数据与测试代码

 

 

4)代码分层

 

 

5)用例执行失败怎么快速判断是bug还是用例数据错误?

 

 

 

 # 我不会碰到难题,没有什么问题是解决不了的,我一般是一些小问题,比如说在写的时候,碰到一些低级的失误,可能某些地方逻辑写错了,具体什么逻辑我不记得了,但是肯定是有这样的场景,逻辑写错了,代码也很深,花了很长时间,最后慢慢慢慢才定为到了,像这种错误是最多的,在功能是线上没有碰到问题

# 没有很深刻的问题就说,这个框架出得最多的就是用例的格式写错了,因为我这个框架封装得非常完成,我只要写用例,不需要写代码,在写用例的过程中经常出现用例的格式写错了

 

6)配置化代码解耦

 

 

7)接口基础知识

 

 

 

 

8)用例编写

 

 

 

 

9)怎么用代码发送请求

 

10)数据库校验

 

 

 

11)测试数据动态生成

 

 

12)接口依赖

 

 

 

 

13)代码进一步解耦

 

 

14)V3版本权限验证

 

 

15)session鉴权

 

16)mock测试

 

 

2.自动化测试开展流程

 

 

1)需求分析

 

 

2)了解接口

 

 

3)自动化框架/工具选择

 

 

4)写接口用例

 

 

5)维护阶段

 

 

 

 

3.接口自动化框架运行流程图

 http://www.processon.com

 

posted @ 2022-11-17 22:05  千秋与  阅读(55)  评论(0)    收藏  举报