接口测框架
安装教程
- 需要3.5及以上版本的python
- pip install -r requirements.txt
使用说明
- 运行manage.py创建项目
- 创建的项目在projects目录下
- 在项目的cases目录下编写测试用例,可以参考litemall项目中如何编写测试用例
- 执行项目目录下的run.py运行所有测试用例
一、config配置文件
三个文件:
const_template.py
run_template.py
setting.py
文件代码:
const_template.py
1 import os 2 3 host = 'http://ip:port' # 测试环境地址 4 5 project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 7 data_path = os.path.join(project_path, 'data') # 存测试数据的目录 8 report_path = os.path.join(project_path, 'report') # 存报告的目录 9 case_path = os.path.join(project_path, 'cases') # 存测试用例的目录
run_template.py
1 from utils.send_message import send_mail 2 from config.setting import email_template 3 from projects.Iitemall.public.const import case_path, report_path 4 import nnreport as bf 5 import datetime 6 import unittest 7 import os 8 import sys 9 10 root_dir = os.path.dirname( 11 os.path.dirname( 12 os.path.dirname( 13 os.path.abspath(__file__)))) 14 # 项目根目录,加入环境变量,否则直接在命令行里面运行的时候有问题, 找不到其他的模块 15 sys.path.insert(0, root_dir) 16 17 18 def run(): 19 test_suite = unittest.defaultTestLoader.discover(case_path, 'test*.py') 20 # 这里是指定找什么开头的.py文件,运行用例的时候可以自己改 21 report = bf.BeautifulReport(test_suite) 22 title = '{project_name}_测试报告' 23 filename = title + '_' + datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '.html' 24 report.report(description=title, 25 filename=filename, 26 log_path=report_path) 27 email_content = email_template.format(pass_count=report.success_count, 28 fail_count=report.failure_count, 29 all_count=report.success_count + report.failure_count) 30 31 report_abs_path = os.path.join(report_path, filename) 32 send_mail(filename, email_content, report_abs_path) 33 34 35 if __name__ == '__main__': 36 run()
setting.py
1 import os 2 import nnlog 3 4 mysql_info = { 5 'default': 6 { 7 'host': 'ip', 8 'port': 3306, 9 'user': 'dbuser', 10 'password': 'dbpassword', 11 'db': 'db', 12 'charset': 'utf8', 13 } 14 } # 数据库配置,多个数据库,在字典里加key就可以了 15 16 redis_info = { 17 'default': { 18 'host': 'ip', 19 'port': 6379, 20 'db': 0, 21 'decode_responses': True 22 } 23 } # redis配置,多个数据库,在字典里加key就可以了 24 25 email_info = { 26 'host': 'smtp.163.com', # 27 'user': '[email protected]', # 用户 28 'password': '5tgb6yhn', # 密码 29 'port': 465, 30 } 31 32 email_to = ['[email protected]'] 33 email_cc = ['[email protected]'] 34 35 base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 36 log_path = os.path.join(base_path, 'logs', 'utp.log') # 指定日志文件 37 projects_path = os.path.join(base_path, 'projects') # 项目目录 38 log = nnlog.Logger(log_path) 39 40 email_template = ''' 41 各位好: 42 本次接口测试结果如下:总共运行{all_count}条用例,通过{pass_count}条,失败【{fail_count}】条。 43 详细信息请查看附件。 44 ''' # 邮件模板
一、utils公共方法
该文件夹为python文件夹,需待__init__.py文件
七个文件:
clean.py
data_util.py
db_util.py
project.py
request.py
send_message.py
utils.py
文件代码:
clean.py
1 import os 2 import time 3 from config.setting import projects_path 4 5 6 def clean_report(days=10): 7 '''清理测试报告''' 8 for cur_dir, dirs, files in os.walk(projects_path): # 递归获取项目目录下所有文件夹 9 if cur_dir.endswith('report'): # 判断如果文件夹是report的话,获取文件夹下面的文件 10 for report in files: 11 if report.endswith('.html'): # 如果是.html结尾的 12 report_path = os.path.join(cur_dir, report) 13 if os.path.getctime( 14 report_path) < time.time() - 60 * 60 * 24 * days: 15 os.remove(report_path)
data_util.py
1 import os 2 import xlrd 3 4 from config.setting import log 5 from .db_util import get_mysql_connect 6 7 8 class GetTestData: 9 @staticmethod 10 def data_for_txt(file_name): 11 ''' 12 从文本文件里面获取参数化数据 13 :param file_name: 文件名 14 :return:二维数组 15 ''' 16 log.debug('开始读取参数化文件%s' % file_name) 17 if os.path.exists(file_name): 18 with open(file_name, encoding='utf-8') as fr: 19 data = [] 20 for line in fr: 21 if line.strip(): 22 line_data = line.strip().split(',') 23 data.append(line_data) 24 return data 25 log.error('%s参数化文件不存在' % file_name) 26 raise Exception('%s参数化文件不存在' % file_name) 27 28 @staticmethod 29 def data_for_excel(file_name, sheet_name=None): 30 ''' 31 从excel里面读参数化数据 32 :param file_name: 文件名 33 :param sheet_name: sheet页名字,默认不写取第一个sheet页 34 :return: 二维数组 35 ''' 36 log.debug('开始读取参数化文件%s' % file_name) 37 if os.path.exists(file_name): 38 data = [] 39 book = xlrd.open_workbook(file_name) 40 if sheet_name: 41 sheet = book.sheet_by_name(sheet_name) 42 else: 43 sheet = book.sheet_by_index(0) 44 for row_num in range(1, sheet.nrows): 45 row_data = sheet.row_values(row_num) 46 data.append(row_data) 47 return data 48 log.error('%s参数化文件不存在' % file_name) 49 raise Exception('%s参数化文件不存在' % file_name) 50 51 @staticmethod 52 def data_for_mysql(sql, db_config='default'): 53 ''' 54 从数据库里面获取测试数据 55 :param sql:sql语句 56 :param db_config:从配置文件里面配置的mysql信息 57 :return:从数据库里面查出来的二维数组 58 ''' 59 mysql = get_mysql_connect(db_config) 60 return mysql.get_list_data(sql)
db_util.py
1 import pymysql 2 import redis 3 from config.setting import mysql_info, redis_info 4 5 6 class Mysql: 7 def __init__(self, host, user, password, db, port=3306, charset='utf8'): 8 # 构造函数,类在实例化的时候会自动执行构造函数 9 self.db_info = {'user': user, 'password': password, "db": db, "port": port, 'charset': charset, 10 'autocommit': True, 'host': host} 11 self.__connect() 12 13 def __del__(self): 14 self.__close() 15 16 def __connect(self): 17 try: 18 self.conn = pymysql.connect(**self.db_info) # 建立连接 19 except Exception as e: 20 raise Exception("连接不上数据库,请检查数据库连接信息") 21 22 else: 23 self.__set_cur() # 设置游标 24 25 def execute_many(self, sql): 26 self.cur.execute(sql) 27 return self.cur.fetchall() 28 29 def execute_one(self, sql): 30 self.cur.execute(sql) 31 return self.cur.fetchone() 32 33 def __set_cur(self, type=pymysql.cursors.DictCursor): # 设置游标,默认是字典类型 34 self.cur = self.conn.cursor(cursor=type) 35 36 def get_list_data(self, sql): 37 '''从数据库获取到的数据是list''' 38 self.__set_cur(type=None) # 设置游标为空,返回的就不是字典了 39 self.cur.execute(sql) 40 self.__set_cur() # 查完之后重新设置游标为字典类型 41 return self.cur.fetchall() 42 43 def __close(self): 44 self.conn.close() 45 self.cur.close() 46 47 48 def get_redis_connect(name='default'): 49 '''获取redis连接,如果不传name,获取默认的链接''' 50 redis_config = redis_info.get(name) 51 return redis.Redis(**redis_config) 52 53 54 def get_mysql_connect(name='default'): 55 '''获取mysql连接,如果不传name,获取默认的链接''' 56 mysql_config = mysql_info.get(name) 57 return Mysql(**mysql_config)
project.py
1 import os 2 3 4 class Project: 5 base_path = os.path.dirname( 6 os.path.dirname( 7 os.path.abspath(__file__))) # 工程目录 8 projects_path = os.path.join(base_path, 'projects') # 项目目录 9 child_dirs = ['cases', 'data', 'report', 'public'] 10 11 def __init__(self, project_name): 12 self.project_name = project_name 13 self.project_path = os.path.join( 14 self.projects_path, project_name) # 要创建的项目目录 15 16 def create_project(self): 17 '''校验项目是否存在,不存在的话,创建''' 18 if os.path.exists(self.project_path): 19 raise Exception("项目已经存在!") 20 else: 21 os.mkdir(self.project_path) 22 23 def create_init_py(self, path): 24 ''' 25 创建__init__.py文件 26 :param path: 路径 27 :return: 28 ''' 29 py_file_path = os.path.join(path, '__init__.py') 30 self.write_content(py_file_path, '') # 打开一个空文件 31 32 def create_dir(self, ): 33 '''创建项目下面的子目录''' 34 for dir in self.child_dirs: 35 dir_path = os.path.join(self.project_path, dir) 36 os.mkdir(dir_path) 37 if dir == 'cases': # 如果是cases文件夹的话,创建__init__.py 38 # cases是个package查找用例的时候才会找到那个目录下所有子目录里面的测试用例 39 self.create_init_py(dir_path) 40 41 def create_run_py(self): 42 '''生成run.py''' 43 run_template_path = os.path.join( 44 self.base_path, 'config', 'run_template') 45 content = self.get_template_content( 46 run_template_path).format(project_name=self.project_name) 47 run_file_path = os.path.join(self.project_path, 'run.py') 48 self.write_content(run_file_path, content) 49 50 def create_const_py(self): 51 '''生成const.py''' 52 run_template_path = os.path.join( 53 self.base_path, 'config', 'const_template') 54 content = self.get_template_content(run_template_path) 55 run_file_path = os.path.join(self.project_path, 'public', 'const.py') 56 self.write_content(run_file_path, content) 57 58 def main(self): 59 '''创建项目''' 60 self.create_project() # 创建项目 61 self.create_dir() # 创建项目下面的文件夹 62 self.create_run_py() # 创建run.py 63 self.create_const_py() # 创建const.py 64 65 @staticmethod 66 def get_template_content(file_name): 67 '''读取文件内容''' 68 with open(file_name, encoding='utf-8') as fr: 69 return fr.read() 70 71 @staticmethod 72 def write_content(file, content): 73 '''写入文件''' 74 with open(file, 'w', encoding='utf-8') as fw: 75 fw.write(content)
request.py
1 import requests 2 # 反射 3 4 5 class MyRequest: 6 def __init__(self, url, method='get', data=None, 7 headers=None, is_json=True): 8 method = method.lower() 9 self.url = url 10 self.data = data 11 self.headers = headers 12 self.is_json = is_json 13 if hasattr(self, method): 14 getattr(self, method)() 15 16 def get(self): 17 try: 18 req = requests.get( 19 self.url, 20 self.data, 21 headers=self.headers).json() 22 except Exception as e: 23 self.response = {"error": "接口请求出错%s" % e} 24 else: 25 self.response = req 26 27 def post(self): 28 try: 29 if self.is_json: 30 req = requests.post( 31 self.url, 32 json=self.data, 33 headers=self.headers).json() 34 else: 35 req = requests.post( 36 self.url, self.data, headers=self.headers).json() 37 except Exception as e: 38 self.response = {"error": "接口请求出错%s" % e} 39 else: 40 self.response = req
send_message.py
1 import yamail 2 import traceback 3 from config.setting import email_info, email_cc, email_to, log 4 5 6 def send_mail(subject, content, files=None): 7 ''' 8 发送邮件 9 :param subject:主题 10 :param content: 内容 11 :param files: 附件 12 :return: 13 ''' 14 try: 15 smtp = yamail.SMTP(**email_info) 16 smtp.send(subject=subject, contents=content, 17 to=email_to, cc=email_cc, attachments=files) 18 except Exception as e: 19 log.error("发送邮件失败+%s" % traceback.format_exc()) 20 21 22 def send_sms(): 23 ''' 24 发送短信验证码 25 :return: 26 ''' 27 pass
utils.py
1 import jsonpath 2 3 4 def get_value(dic, key): 5 ''' 6 这个函数是从一个字典里面,根据key获取vlaue 7 :param dic:传一个字典 8 :param key:传一个 9 :return:如果有,返回key取到value,如果key没有,返回空字符串 10 ''' 11 result = jsonpath.jsonpath(dic, '$..%s' % key) 12 if result: 13 return result[0] 14 return ''
三、 Projects项目模块(litemall项目)
四个文件夹和一个python文件:
cases(python文件夹)
test_address.py
test_coupon.py
data (存放测试数据)
address.xlsx
goods.txt
public
const.py (存放测试常量)
tools.py
report
run.py
文件代码:
test_address.py
1 import unittest 2 import parameterized 3 import os 4 from urllib.parse import urljoin 5 from projects.litemall.public.const import host, test_user, data_path 6 from projects.litemall.public import tools 7 from utils.request import MyRequest 8 from utils.db_util import get_mysql_connect 9 from utils.data_util import GetTestData 10 11 address_data_path = os.path.join(data_path, 'address.xlsx') # 拼接测试数据文件的路径 12 test_address_data = GetTestData.data_for_excel(address_data_path) # 获取参数化使用的数据 13 14 15 class TestAddress(unittest.TestCase): 16 url = urljoin(host, '/wx/address/save') 17 18 @classmethod 19 def setUpClass(cls): 20 # cls.mysql = get_mysql_connect() # 获取mysql连接 21 token = tools.WxLogin(**test_user).get_token() # 登录获取token 22 cls.header = {'X-Litemall-Token': token} # 拼header 23 24 @parameterized.parameterized.expand(test_address_data) # 参数化 25 def test_create(self, name, tel, isDefault): 26 '''测试添加收货地址''' 27 is_default = True if isDefault == '1' else False 28 data = { 29 "name": name, 30 "tel": "%d" % tel, 31 "country": "", 32 "province": "北京市", 33 "city": "市辖区", 34 "county": "东城区", 35 "areaCode": "110101", 36 "postalCode": "", 37 "addressDetail": "西二旗", 38 "isDefault": is_default 39 } 40 req = MyRequest( 41 self.url, 42 'post', 43 data=data, 44 headers=self.header) # 发请求 45 self.assertEqual(0, req.response.get('errno'), msg='添加失败') # 校验错误码是否为0 46 address_id = req.response.get('data') 47 # sql = 'select name from litemall_address where id = %s;' % address_id 48 # db_data = self.mysql.execute_one(sql) 49 # self.assertIsNotNone(db_data, msg='litemall:查询地址不存在')#校验是否从数据库查到数据 50 # self.assertEqual(db_data.get('name'), name) #判断数据库存的名字和添加的名字是否一样
test_coupon.py
1 import unittest 2 from urllib.parse import urljoin 3 from projects.Iitemall.public.const import host, test_admin_user 4 from projects.Iitemall.public import tools 5 from utils.request import MyRequest 6 from utils.utils import get_value 7 8 9 class TestCoupon(unittest.TestCase): 10 11 @classmethod 12 def setUpClass(cls): 13 token = tools.AdminLogin(**test_admin_user).get_token() # 登录获取token 14 cls.header = {'X-Litemall-Admin-Token': token} # 拼header 15 16 def add_coupon(self): 17 url = urljoin(host, '/admin/coupon/create') 18 name = 'Python自动化测试优惠券' 19 data = { 20 "name": name, 21 "desc": "介绍", 22 "total": "29", 23 "discount": "1", 24 "min": "999", 25 "limit": 1, 26 "type": 0, 27 "status": 0, 28 "goodsType": 0, 29 "goodsValue": [], 30 "timeType": 0, 31 "days": "1", 32 "startTime": None, 33 "endTime": None 34 } 35 req = MyRequest(url, 'post', data=data, headers=self.header) 36 print(req.response) 37 self.assertEqual(0, req.response.get('errno'), msg='添加失败') 38 coupon_id = get_value(req.response, 'id') 39 return name, coupon_id 40 41 def test_coupon(self): 42 '''测试添加优惠券后,在首页是否查到''' 43 url = urljoin(host, '/wx/coupon/list') 44 name, id = self.add_coupon() # 添加优惠券 45 req = MyRequest(url) 46 coupon_list = get_value(req.response, 'list') 47 tag = False 48 for coupon in coupon_list: 49 if name == coupon.get('name') and coupon.get('id') == id: 50 tag = True 51 break 52 self.assertTrue(tag, msg='添加的优惠券查不到')
const.py
1 import os 2 host = 'http://proxy.nnzhp.cn' 3 4 test_user = { 5 'username': 'user123', 6 'password': 'user123' 7 } # 测试用户 8 9 test_admin_user = { 10 'username': 'admin123', 11 'password': 'admin123' 12 } # 测试用户 13 14 project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 15 16 data_path = os.path.join(project_path, 'data') # 存测试数据的目录 17 report_path = os.path.join(project_path, 'report') # 存报告的目录 18 case_path = os.path.join(project_path, 'cases') # 存测试用例的目录
tools.py
1 from utils.utils import get_value 2 from utils.request import MyRequest 3 from config.setting import log 4 from urllib import parse 5 from .const import host 6 7 8 class AdminLogin: 9 '''admin登录''' 10 url = parse.urljoin(host, '/admin/auth/login') # 拼接url 11 12 def __init__(self, username, password): 13 self.username = username 14 self.password = password 15 16 def get_token(self): 17 data = {'username': self.username, 'password': self.password} 18 req = MyRequest(self.url, 'post', data=data, is_json=True) 19 token = get_value(req.response, 'token') 20 log.debug("登录的返回结果,%s" % req.response) 21 if token: 22 return token 23 log.error('litemall:登录失败' % req.response) 24 raise Exception('登录失败,错误信息%s' % req.response) 25 26 27 class WxLogin(AdminLogin): 28 '''Wx登录''' 29 url = parse.urljoin(host, '/wx/auth/login')
run.py
1 import unittest 2 import datetime 3 import os 4 import nnreport 5 from projects.Iitemall.public.const import case_path, report_path 6 from config.setting import email_template 7 from utils.send_message import send_mail 8 9 10 def run(): 11 test_suite = unittest.defaultTestLoader.discover(case_path, 'test*.py') 12 # 这里是指定找什么开头的.py文件,运行用例的时候可以自己改 13 report = nnreport.BeautifulReport(test_suite) 14 title = 'litemall_测试报告' 15 filename = title + '_' + datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '.html' 16 report.report(description=title, 17 filename=filename, 18 log_path=report_path) 19 email_content = email_template.format(pass_count=report.success_count, 20 fail_count=report.failure_count, 21 all_count=report.success_count + report.failure_count) 22 23 report_abs_path = os.path.join(report_path, filename) 24 send_mail(filename, email_content, report_abs_path) 25 26 27 if __name__ == '__main__': 28 run()
四、必装第三方模块
requirement.txt
nnreport
pymysql
yamail
requests
jsonpath
nnlog
xlrd
redis
parameterized