python+requests+pytest+allure自動化框架

核心庫

  1. requests request請求
  2. openpyxl excel文件操做
  3. loggin 日誌
  4. smtplib 發送郵件
  5. configparser
  6. unittest.mock mock服務

目錄結構

  1. base
  2. utils
  3. testDatas
  4. conf
  5. testCases
  6. testReport
  7. logs
  8. 其餘

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

(只取核心部分)html

  • 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
    • 步驟
      1. 收集用例
      2. 執行用例
      3. 斷言
      4. 構造測試報告
      5. 發送郵件

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

總結

  1. allure有不少有趣的操做,甚至控制用例執行行爲,有興趣能夠拓展,也能夠看下以前的博客
  2. 實現框架的難點在接口依賴
  3. 接口自動化應避免複雜的接口依賴,複雜的依賴只會形成測試的不可控性
  4. 注意頻繁的操做excel會消耗性能
  5. 有興趣能夠將本框架集合在Jenkins中
  6. 本文的demo接口均採用至本站,感謝做者提供的免費接口
  7. 項目git地址:...(git加密了,後續補上))
相關文章
相關標籤/搜索