核心庫
- requests request請求
- openpyxl excel文件操作
- loggin 日志
- smtplib 發送郵件
- configparser
- unittest.mock mock服務
目錄結構
base
- base_path.py 存放絕對路徑,dos命令或Jenkins執行時,防止報錯
- base_requests.py 封裝requests,根據method選擇不同的方法執行腳本,同時處理請求異常
base_path.py
import os
# 項目根路徑
_root_path = os.path.split(os.path.split(os.path.realpath(__file__))[0])[0]
# 報告路徑
report_path = os.path.join(_root_path, 'testReport', 'report.html')
# 日志路徑
log_path = os.path.join(_root_path, 'logs/')
# 配置文件路徑
conf_path = os.path.join(_root_path, 'conf', 'auto_test.conf')
# 測試數據路徑
testdatas_path = os.path.join(_root_path, 'testDatas')
# allure 相關配置
_result_path = os.path.join(_root_path, 'testReport', 'result')
_allure_html_path = os.path.join(_root_path, 'testReport', 'allure_html')
allure_command = 'allure generate {} -o {} --clean'.format(_result_path, _allure_html_path)
base_requests.py
import json
import allure
import urllib3
import requests
import warnings
from bs4 import BeautifulSoup
from base.base_path import *
from requests.adapters import HTTPAdapter
from utils.handle_logger import logger
from utils.handle_config import handle_config as hc
class BaseRequests:
def __init__(self, case, proxies=None, headers=None, cookies=None, timeout=15, max_retries=3):
'''
:param case: 測試用例
:param proxies: The result is displayed in fiddler:
{"http": "http://127.0.0.1:8888", "https": "https://127.0.0.1:8888"}
:param headers: 請求頭
:param cookies: cookies
:param timeout: 請求默認超時時間15s
:param max_retries: 請求超時后默認重試3次
'''
self.case = case
self.proxies = proxies
self.headers = headers
self.cookies = cookies
self.timeout = timeout
self.max_retries = max_retries
self.base_url = hc.operation_config(conf_path, 'BASEURL', 'base_url')
def get_response(self):
'''獲取請求結果'''
response = self._run_main()
return response
def _run_main(self):
'''發送請求'''
method = self.case['method']
url = self.base_url + self.case['url']
if self.case['parameter']:
data = eval(self.case['parameter'])
else:
data = None
s = requests.session()
s.mount('http://', HTTPAdapter(max_retries=self.max_retries))
s.mount('https://', HTTPAdapter(max_retries=self.max_retries))
urllib3.disable_warnings() # 忽略瀏覽器認證(https認證)警告
warnings.simplefilter('ignore', ResourceWarning) # 忽略 ResourceWarning警告
res=''
if method.upper() == 'POST':
try:
res = s.request(method='post', url=url, data=data, verify=False, proxies=self.proxies, headers=self.headers, cookies=self.cookies, timeout=self.timeout)
except Exception as e:
logger.error('POST請求出錯,錯誤信息為:{0}'.format(e))
elif method.upper() == 'GET':
try:
res = s.request(method='get', url=url, params=data, verify=False,proxies=self.proxies, headers=self.headers, cookies=self.cookies, timeout=self.timeout)
except Exception as e:
logger.error('GET請求出錯,錯誤信息為:{0}'.format(e))
else:
raise ValueError('method方法為get和post')
logger.info(f'請求方法:{method},請求路徑:{url}, 請求參數:{data}, 請求頭:{self.headers}, cookies:{self.cookies}')
# with allure.step('接口請求信息:'):
# allure.attach(f'請求方法:{method},請求路徑:{url}, 請求參數:{data}, 請求頭:{headers}')
# 拓展:是否需要做全量契約驗證?響應結果是不同類型時,如何處理響應?
return res
if __name__ == '__main__':
# case = {'method': 'get', 'url': '/article/top/json', 'parameter': ''}
case = {'method': 'post', 'url': '/user/login', 'parameter': '{"username": "xbc", "password": "123456"}'}
response = BaseRequests(case).get_response()
print(response.json())
utils
(只取核心部分)
- handle_excel.py
- excel的操作,框架要求,最終讀取的數據需要保存列表嵌套字典的格式[{},{}]
- 其他操作
- handle_sendEmail.py
- python發送郵件使用smtp協議,接收郵件使用pop3
- 需要開啟pop3服務功能,這里的password為授權碼,啟用服務自行百度
- handle_logger.py 日志處理
- handle_config.py
- 配置文件處理,這里只將域名可配置化,切換環境時改域名即可
- handle_allure.py
- allure生成的報告需要調用命令行再打開,這里直接封裝命令
- handle_cookies.py(略)
- 在git中補充,處理cookiesJar對象
- handle_mock.py(略)
- 在git中補充,框架未使用到,但是也封裝成了方法
- param_replace(略)
- 將常用的參數化操作封裝成類
handle_excel.py
import openpyxl
from base.base_path import *
class HandleExcel:
def __init__(self, file_name=None, sheet_name=None):
'''
沒有傳路徑時,默認使用 wanadriod接口測試用例.xlsx 文件
:param file_name: 用例文件
:param sheet_name: 表單名
'''
if file_name:
self.file_path = os.path.join(testdatas_path, file_name)
self.sheet_name = sheet_name
else:
self.file_path = os.path.join(testdatas_path, 'wanadriod接口測試用例.xlsx')
self.sheet_name = 'case'
# 創建工作簿,定位表單
self.wb = openpyxl.load_workbook(self.file_path)
self.sheet = self.wb[self.sheet_name]
# 列總數,行總數
self.ncols = self.sheet.max_column
self.nrows = self.sheet.max_row
def cell_value(self, row=1, column=1):
'''獲取表中數據,默認取出第一行第一列的值'''
return self.sheet.cell(row, column).value
def _get_title(self):
'''私有函數, 返回表頭列表'''
title = []
for column in range(1, self.ncols+1):
title.append(self.cell_value(1, column))
return title
def get_excel_data(self):
'''
:return: 返回字典套列表的方式 [{title_url:value1, title_method:value1}, {title_url:value2, title_method:value2}...]
'''
finally_data = []
for row in range(2, self.nrows+1):
result_dict = {}
for column in range(1, self.ncols+1):
result_dict[self._get_title()[column-1]] = self.cell_value(row, column)
finally_data.append(result_dict)
return finally_data
def get_pytestParametrizeData(self):
'''
選用這種參數方式,需要使用數據格式 列表套列表 @pytest.mark.parametrize('', [[], []]), 如 @pytest.mark.parametrize(*get_pytestParametrizeData)
將 finally_data 中的 title 取出,以字符串形式保存,每個title用逗號(,)隔開
將 finally_data 中的 value 取出,每行數據保存在一個列表,再集合在一個大列表內
:return: title, data
'''
finally_data = self.get_excel_data()
data = []
title = ''
for i in finally_data:
value_list = []
key_list = []
for key, value in i.items():
value_list.append(value)
key_list.append(key)
title = ','.join(key_list)
data.append(value_list)
return title, data
def rewrite_value(self, new_value, case_id, title):
'''寫入excel,存儲使用過的數據(參數化后的數據)'''
row = self.get_row(case_id)
column = self.get_column(title)
self.sheet.cell(row, column).value = new_value
self.wb.save(self.file_path)
def get_row(self, case_id):
'''通過執行的 case_id 獲取當前的行號'''
for row in range(1, self.nrows+1):
if self.cell_value(row, 1) == case_id:
return int(row)
def get_column(self, title):
'''通過表頭給定字段,獲取表頭所在列'''
for column in range(1, self.ncols+1):
if self.cell_value(1, column) == title:
return int(column)
if __name__ == '__main__':
r = HandleExcel()
print(r.get_excel_data())
handle_sendEmail.py
import smtplib
from utils.handle_logger import logger
from email.mime.text import MIMEText # 專門發送正文郵件
from email.mime.multipart import MIMEMultipart # 發送正文、附件等
from email.mime.application import MIMEApplication # 發送附件
class HandleSendEmail:
def __init__(self, part_text, attachment_list, password, user_list, subject='interface_autoTestReport', smtp_server='smtp.163.com', from_user='hu_chunpu@163.com', filename='unit_test_report.html'):
'''
:param part_text: 正文
:param attachment_list: 附件列表
:param password: 郵箱服務器第三方密碼
:param user_list: 收件人列表
:param subject: 主題
:param smtp_server: 郵箱服務器
:param from_user: 發件人
:param filename: 附件名稱
'''
self.subject = subject
self.attachment_list = attachment_list
self.password = password
self.user_list = ';'.join(user_list) # 多個收件人
self.part_text = part_text
self.smtp_server = smtp_server
self.from_user = from_user
self.filename = filename
def _part(self):
'''構建郵件內容'''
# 1) 構造郵件集合體:
msg = MIMEMultipart()
msg['Subject'] = self.subject
msg['From'] = self.from_user
msg['To'] = self.user_list
# 2) 構造郵件正文:
text = MIMEText(self.part_text)
msg.attach(text) # 把正文加到郵件體里面
# 3) 構造郵件附件:
for item in self.attachment_list:
with open(item, 'rb+') as file:
attachment = MIMEApplication(file.read())
# 給附件命名:
attachment.add_header('Content-Disposition', 'attachment', filename=item)
msg.attach(attachment)
# 4) 得到完整的郵件內容:
full_text = msg.as_string()
return full_text
def send_email(self):
'''發送郵件'''
# qq郵箱必須加上SSL
if self.smtp_server == 'smtp.qq.com':
smtp = smtplib.SMTP_SSL(self.smtp_server)
else:
smtp = smtplib.SMTP(self.smtp_server)
# 登錄服務器:.login(user=email_address,password=第三方授權碼)
smtp.login(self.from_user, self.password)
logger.info('--------郵件發送中--------')
try:
logger.info('--------郵件發送成功--------')
smtp.sendmail(self.from_user, self.user_list, self._part())
except Exception as e:
logger.error('發送郵件出錯,錯誤信息為:{0}'.format(e))
else:
smtp.close() # 關閉連接
if __name__ == '__main__':
from base.base_path import *
part_text = '附件為自動化測試報告,框架使用了pytest+allure'
attachment_list = [report_path]
password = ''
user_list = ['']
HandleSendEmail(part_text, attachment_list, password, user_list).send_email()
handle_logger.py
import sys
import logging
from time import strftime
from base.base_path import *
class Logger:
def __init__(self):
# 日志格式
custom_format = '%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s: %(message)s'
# 日期格式
date_format = '%a, %d %b %Y %H:%M:%S'
self._logger = logging.getLogger() # 實例化
self.filename = '{0}{1}.log'.format(log_path, strftime("%Y-%m-%d")) # 日志文件名
self.formatter = logging.Formatter(fmt=custom_format, datefmt=date_format)
self._logger.addHandler(self._get_file_handler(self.filename))
self._logger.addHandler(self._get_console_handler())
self._logger.setLevel(logging.INFO) # 默認等級
def _get_file_handler(self, filename):
'''輸出到日志文件'''
filehandler = logging.FileHandler(filename, encoding="utf-8")
filehandler.setFormatter(self.formatter)
return filehandler
def _get_console_handler(self):
'''輸出到控制台'''
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(self.formatter)
return console_handler
@property
def logger(self):
return self._logger
'''
日志級別:
critical 嚴重錯誤,會導致程序退出
error 可控范圍內的錯誤
warning 警告信息
info 提示信息
debug 調試程序時詳細輸出的記錄
'''
# 實例
logger = Logger().logger
if __name__ == '__main__':
import datetime
logger.info(u"{}:開始XXX操作".format(datetime.datetime.now()))
handle_config.py
import configparser
# 配置文件類
class HandleConfig:
def operation_config(self, conf_file, section, option):
cf = configparser.ConfigParser() # 實例化
cf.read(conf_file)
value = cf.get(section, option) # 定位
return value
handle_config = HandleConfig()
if __name__ == '__main__':
from base.base_path import *
base_url = handle_config.operation_config(conf_path, 'BASEURL', 'base_url')
print(base_url)
handle_allure.py
import subprocess
from base.base_path import *
class HandleAllure(object):
def execute_command(self):
subprocess.call(allure_command, shell=True)
handle_allure = HandleAllure()
testDatas
- excel測試用例文件,
必須是.xlsx結尾
,用例結構如下:
conf
- 放置配置文件
.conf結尾
testCases
- conftest.py
- fixture功能,用例前置后置操作
- 構造測試數據
- 其他高級操作
- 注意郵件中的password和user_list需要換成自己測試的郵箱及服務密碼
- test_wanAndroid.py 測試用例腳本
- 參數化: pytest.mark.parametrize('case',[{},{}])
- 接口關聯:
- 將關聯的參數配置成全局變量
- 在用例執行前使用全局變量替換參數
- 使用 is_run 參數指明有參數化的用例,並取出,再賦值給全局變量
- cookies:
- 和接口關聯的處理方式一樣處理cookies
- 步驟
- 收集用例
- 執行用例
- 斷言
- 構造測試報告
- 發送郵件
conftest.py
import pytest
from base.base_path import *
from utils.handle_logger import logger
from utils.handle_allure import handle_allure
from utils.handle_sendEmail import HandleSendEmail
'''
1. 構造測試數據??
2. fixture 替代 setup,teardown
3. 配置 pytest
'''
def pytest_collection_modifyitems(items):
"""
測試用例收集完成時,將收集到的item的name和nodeid的中文顯示在控制台上
"""
for item in items:
item.name = item.name.encode("utf-8").decode("unicode_escape")
item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
# print(item.nodeid)
@pytest.fixture(scope='session', autouse=True)
def send_email():
logger.info('-----session級,執行wanAndroid測試用例-----')
yield
logger.info('-----session級,wanAndroid用例執行結束,發送郵件:-----')
"""執行alllure命令 """
handle_allure.execute_command()
# 發郵件
part_text = '附件為自動化測試報告,框架使用了pytest+allure'
attachment_list = [report_path]
password = ''
user_list = ['']
HandleSendEmail(part_text, attachment_list, password, user_list).send_email()
test_wanAndroid.py
import json
import pytest
import allure
from base.base_requests import BaseRequests
from utils.handle_logger import logger
from utils.handle_excel import HandleExcel
from utils.param_replace import pr
from utils.handle_cookies import get_cookies
handle_excel = HandleExcel()
get_excel_data = HandleExcel().get_excel_data()
ID = ''
COOKIES = {}
PAGE = ''
class TestWanAndroid:
@pytest.mark.parametrize('case', get_excel_data)
def test_wanAndroid(self, case):
global ID
global COOKIES
# 參數替換
case['url'] = pr.relevant_parameter(case['url'], '${collect_id}', str(ID))
if case['is_run'].lower() == 'yes':
logger.info('------執行用例的id為:{0},用例標題為:{1}------'.format(case['case_id'], case['title']))
res = BaseRequests(case, cookies=COOKIES).get_response()
res_json = res.json()
# 獲取登錄后的cookies
if case['case_id'] == 3:
COOKIES = get_cookies.get_cookies(res)
if case['is_depend']:
try:
ID = res_json['data']['id']
# 將使用的參數化后的數據寫入excel
handle_excel.rewrite_value('id={}'.format(ID), case['case_id'], 'depend_param')
except Exception as e:
logger.error(f'獲取id失敗,錯誤信息為{e}')
ID = 0
# 制作 allure 報告
allure.dynamic.title(case['title'])
allure.dynamic.description('<font color="red">請求URL:</font>{}<br />'
'<font color="red">期望值:</font>{}'.format(case['url'], case['excepted']))
allure.dynamic.feature(case['module'])
allure.dynamic.story(case['method'])
result=''
try:
assert eval(case['excepted'])['errorCode'] == res_json['errorCode']
result = 'pass'
except AssertionError as e:
logger.error('Assert Error:{0}'.format(e))
result = 'fail'
raise e
finally:
# 將實際結果格式化寫入excel
handle_excel.rewrite_value(json.dumps(res_json, ensure_ascii=False, indent=2, sort_keys=True), case['case_id'], 'actual')
# 將用例執行結果寫入excel
handle_excel.rewrite_value(result, case['case_id'], 'test_result')
def test_get_articleList(self):
'''翻頁,將page參數化'''
global PAGE
pass
def test_mock_demo(self):
'''使用mock服務模擬服務器響應'''
pass
if __name__ == '__main__':
pytest.main(['-q', 'test_wanAndroid.py'])
testReport
- 存放html測試報告,安裝插件
pip install pytest-html
- 存放allure測試報告,插件安裝
pip install allure-pytest
logs
- 存放日志文件
其他文件
- run.py 主運行文件
- pytest.ini 配置pytest的默認行為,運行規則等
- requirements.txt 依賴環境
- 自動生成
pip freeze
- 安裝
pip -r install requirements.txt
- 自動生成
總結
- allure有很多有趣的操作,甚至控制用例執行行為,有興趣可以拓展,也可以看下之前的博客
- 實現框架的難點在接口依賴
- 接口自動化應避免復雜的接口依賴,復雜的依賴只會造成測試的不可控性
- 注意頻繁的操作excel會消耗性能
- 有興趣可以將本框架集合在Jenkins中
- 本文的demo接口均采用至本站,感謝作者提供的免費接口
- 項目git地址:...(git加密了,后續補上))