pytest+allure+yaml接口自动化框架搭建

整理下整个接口自动化框架的思路
主要用到的插件:pytest,allure-pytest,yaml

项目框架:

在这里插入图片描述
思维导图:
在这里插入图片描述
开始搭建框架

第一步: Conf 配置

创建config.ini文件:根据需要来去设计类别和类别下的元素
在这里插入图片描述

创建和编写config.py 文件:自定义一些get,set,add方法,用来读取,编辑,添加配置文件中的数据

import configparser
from Common import Log
import os

proDir = os.path.split(os.path.realpath(__file__))[0]
configPath = os.path.join(proDir, "config.ini")


class Read_config:

    def __init__(self):
        self.config = configparser.ConfigParser()
        self.config.read(configPath)

    def get_global(self, param):
        value = self.config.get('global_paras', param)
        return value

    def get_mail(self, param):
        value = self.config.get('mail', param)
        return value

    def get_database(self, param):
        value = self.config.get('database', param)
        return value

    def get_conf(self, section, param):
        value = self.config.get(section, param)
        return value

    def set_conf(self, section, value, text):
        """
        配置文件修改
        :param section:
        :param value:
        :param text:
        :return:
        """
        self.config.set(section, value, text)
        with open(configPath, "w+") as f:
            return self.config.write(f)

    def add_conf(self, section_name):
        """
        添加类别到配置环境里
        :param section_name:
        :return:
        """
        self.config.add_section(section_name)
        with open(configPath, "w+") as f:
            return self.config.write(f)

第二步:编写测试用例

安装插件:

pip install PyYAML

在这里插入图片描述
说明:

  • 创建测试用例.yaml : 里面的结构可以自由设计,只需要了解以下yaml文件的基本语法就好了

编写读取yaml文件的方法:将方法写在通用类Utils里面

import yaml

  def read_data_from_file(file_name):
        """
        读取yaml文件的内容
        :param file_name
        """
        f = open(rootPath + '/data/' + file_name, encoding='utf-8')
        res_json = yaml.load(f, Loader=yaml.FullLoader)  # 添加loader参数是为了去掉load warning
        return res_json

第三步:封装request和assertion方法

安装request插件:

pip install request

创建Customize_request: 请求前的数据处理自行设计,请求后,获取返回的body,耗时,状态码,放入到字典中,该方法返回一个字典

"""
封装request

"""

import requests
from Common import Token
from Common.Utils import Utils
from Conf import Config
from Common import Log


class Request(Utils):

    def __init__(self, env):
        """
        :param env:
        """
        self.env = env
        self.config = Config.Read_config()
        self.log = Log.MyLog()
        self.t = Token.Token()

    def post_request(self, url, data):
        """
        Post请求
        :param url:
        :param data:
        :return:

        """

        # post 请求
        try:
            response = requests.post(url=request_url, params=data)

        except Exception as e:
            print('%s%s' % ('Exception url: ', request_url))
            print(e)
            return ()

        # time_consuming为响应时间,单位为毫秒
        time_consuming = response.elapsed.microseconds / 1000
        # time_total为响应时间,单位为秒
        time_total = response.elapsed.total_seconds()

        response_dicts = dict()
        response_dicts['code'] = response.status_code
        try:
            response_dicts['body'] = response.json()
        except Exception as e:
            print(e)
            response_dicts['body'] = ''

        response_dicts['text'] = response.text
        response_dicts['time_consuming'] = time_consuming
        response_dicts['time_total'] = time_total

        return response_dicts


创建Cutomize_assertion方法:

"""
封装Assert方法

"""
from Common import Log
import json
import traceback
from Conf import Config


class Assertions:
    def __init__(self):
        self.log = Log.MyLog()

    @staticmethod
    def assert_status_code(status_code, expected_code):
        """
        验证response状态码
        :param status_code:
        :param expected_code:
        :return:
        """
        try:
            assert status_code == expected_code
            return True

        except Exception:
            log_error(traceback.format_exc(), status_code, expected_code)
            raise

    @staticmethod
    def assert_single_item(single_item, expected_results):
        """
        验证response body中任意属性的值
        :param single_item:
        :param expected_results:
        :return:
        """
        try:
            assert single_item == expected_results
            return True

        except Exception:
            log_error(traceback.format_exc(), single_item, expected_results)
            raise

    @staticmethod
    def assert_in_text(body, expected_results):
        """
        验证response body中是否包含预期字符串
        :param body:
        :param expected_results:
        :return:
        """
        text = json.dumps(body, ensure_ascii=False)
        try:
            # print(text)
            assert expected_results in text
            return True

        except Exception:
            log_error(traceback.format_exc(), text, expected_results)
            raise

    @staticmethod
    def assert_items(d_body, expected_results):
        """
        验证body里面的items是否符合期望
        需保证expected_results中是属性,在body中都能找到
        :param d_body: 一个dict/json
        :param expected_results: 一个dict/json
        :return:
        """
        for key, value in expected_results.items():
            try:
                if key in d_body.keys():
                    assert d_body[key] == value
                    return True
                else:
                    return False
            except Exception:
                log_error(traceback.format_exc(), d_body[key], value)
                raise

    @staticmethod
    def assert_time(actual_time, expected_time):
        """
        验证response body响应时间小于预期最大响应时间,单位:毫秒
        :param actual_time:
        :param expected_time:
        :return:
        """
        try:
            assert actual_time < expected_time
            return True

        except Exception:
            log_error(traceback.format_exc(), actual_time, expected_time)
            raise


def log_error(e, actual, expected):
    Log.MyLog.error(str(e))
    Log.MyLog.error('actual results is %s, expected results is %s ' % (actual, expected))
    config = Config.Read_config()
    config.set_conf('results', 'final_results', 'False')

第四步:编写log文件(在网上随便复制的)


"""
封装log方法
"""

import logging
import os
import time

LEVELS = {
    'debug': logging.DEBUG,
    'info': logging.INFO,
    'warning': logging.WARNING,
    'error': logging.ERROR,
    'critical': logging.CRITICAL
}

logger = logging.getLogger()
level = 'default'


def create_file(filename):
    path = filename[0:filename.rfind('/')]
    if not os.path.isdir(path):
        os.makedirs(path)
    if not os.path.isfile(filename):
        fd = open(filename, mode='w', encoding='utf-8')
        fd.close()
    else:
        pass


def set_handler(levels):
    if levels == 'error':
        logger.addHandler(MyLog.err_handler)
    logger.addHandler(MyLog.handler)


def remove_handler(levels):
    if levels == 'error':
        logger.removeHandler(MyLog.err_handler)
    logger.removeHandler(MyLog.handler)


def get_current_time():
    return time.strftime(MyLog.date, time.localtime(time.time()))


class MyLog:
    path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    log_file = path+'/Log/log.log'
    err_file = path+'/Log/err.log'
    logger.setLevel(LEVELS.get(level, logging.NOTSET))
    create_file(log_file)
    create_file(err_file)
    date = '%Y-%m-%d %H:%M:%S'

    handler = logging.FileHandler(log_file, encoding='utf-8')
    err_handler = logging.FileHandler(err_file, encoding='utf-8')

    @staticmethod
    def debug(log_meg):
        set_handler('debug')
        logger.debug("[DEBUG " + get_current_time() + "]" + log_meg)
        remove_handler('debug')

    @staticmethod
    def info(log_meg):
        set_handler('info')
        logger.info("[INFO " + get_current_time() + "]" + log_meg)
        remove_handler('info')

    @staticmethod
    def warning(log_meg):
        set_handler('warning')
        logger.warning("[WARNING " + get_current_time() + "]" + log_meg)
        remove_handler('warning')

    @staticmethod
    def error(log_meg):
        set_handler('error')
        logger.error("[ERROR " + get_current_time() + "]" + log_meg)
        remove_handler('error')

    @staticmethod
    def critical(log_meg):
        set_handler('critical')
        logger.error("[CRITICAL " + get_current_time() + "]" + log_meg)
        remove_handler('critical')


if __name__ == "__main__":
    MyLog.debug("This is debug message")
    MyLog.info("This is info message")
    MyLog.warning("This is warning message")
    MyLog.error("This is error")
    MyLog.critical("This is critical message")

第五步:编写测试用例脚本

安装pytest和allure-pytest:

pip install pytest 
pip install allure-pytest 

系统安装allure:

brew install  allure (mac系统)

测试用例脚本:

import os
import sys
import pytest
import allure

# 避免使用命令行运行时,找不到自定义的module,需要添加下module的绝对路径
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)

print(sys.path)
from Common.Utils import Utils
from Common.Customize_request import Request
from Common.Customize_assertion import Assertions
from Conf import Config


@pytest.mark.parametrize('case', Utils.read_data_from_file('user_details.yaml'))
@allure.feature('Personal Center')
@allure.severity('normal')
@allure.story('obtain the user personal information')
def test_user_details(case):
    # 动态定制每个测试用例的title
    allure.dynamic.title(case['case_id'])
    allure.dynamic.description(case['title'])

    config = Config.Read_config()
    r = Request(config.get_global('current_environment'))
    test = Assertions()

    response = r.post_request(case['url'], case['data'])
    print(response)
    assert test.assert_status_code(response['code'], case['returns']['code'])
    assert test.assert_single_item(response['body']['data']['yogoId'], case['returns']['validator']['yogoId'])
    assert test.assert_in_text(response['body'], 'msg')
    assert test.assert_items(response['body']['data'], case['returns']['validator'])


if __name__ == "__main__":
    pytest.main(["-s", "test_user_details.py"])

说明:

  • pytest的参数化,从每一个yaml文件中读取的,都是测试用例的集合,通过pytest的参数化,循坏运行每个测试用例;
  • 定义allure里的的参数,来生成定制化的测试报告;
  • 对请求返回的结果进行验证

第六步:运行测试用例

运行单个测试用例,把下面的代码,复制到单个测试用例脚本下,就可以实时看到结果:

if __name__ == "__main__":
    pytest.main(["-s", "test_user_details.py"])

安装插件(运行失败可设置重新运行N次):

pip install pytest-rerunfailures

运行多个测试用例,创建Run.py文件:

import os
import time
from Common import Log
from Conf import Config
from Common import Email
from Common.Utils import Utils

current_path = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(current_path)[0]


if __name__ == '__main__':

    Utils.remove_dir_and_its_files(current_path + '/outputs')

    # 获取当前的时间
    current_time = time.strftime('%Y%m%d%H%M%S')
    log = Log.MyLog()
    config = Config.Read_config()

    log.info('初始化配置文件: ' + Config.configPath)
    config.set_conf('results', 'final_results', 'True')

    # 运行测试用例,并生成测试报告文件 xml格式
    # 重新运行上一次失败的case,重试2次: --reruns 2
    print(current_path)
    os.system('pytest --alluredir=outputs/results/results_' + current_time)
    os.system('allure generate outputs/results/results_' + current_time + ' -o outputs/reports_' + current_time)

到此,基础的框架就完成了

进阶设置:

邮件通知:

"""
封装发送邮件的方法

"""
import smtplib
import time
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from Common.Utils import Utils

from Common import Log
from Conf import Config


class SendMail(Utils):

    def __init__(self):
        super().__init__()
        self.config = Config.Read_config()
        self.log = Log.MyLog()

    def send_mail(self):

        # 第三方服务
        mail_host = self.config.get_mail('mail_host')  # 设置服务器
        mail_user = self.config.get_mail('mail_user')  # 用户名
        mail_pass = self.config.get_mail('mail_pass')  # 口令

        sender = self.config.get_mail('sender')
        receivers = self.config.get_mail('receiver')

        # msg = MIMEMultipart()

        body = 'Hi,all\n接口自动化测试完毕,失败case如下:\n'

        with open(self.projectDir + "/fail_cases.txt", 'r', encoding='utf-8') as f:
            while True:
                line = f.readline()
                body += line.strip() + '\n'
                if not line:
                    break

        message = MIMEMultipart()
        tm = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))

        message['Subject'] = Header("接口自动化测试报告" + "_" + tm, 'utf-8')
        message['From'] = Header("herby he", 'utf-8')  # 邮件里展示用户名
        message['To'] = receivers

        message.attach(MIMEText(body, 'plain', 'utf-8'))

        att1 = MIMEText(open(self.projectDir + '/Log/err_mail.log', 'rb').read(), 'base64', 'utf-8')
        att1["Content-Type"] = 'application/octet-stream'
        # 这里的filename可以任意写,写什么名字,邮件中显示什么名字
        att1["Content-Disposition"] = 'attachment; filename="log.txt"'
        message.attach(att1)

        try:
            smtp_obj = smtplib.SMTP()
            smtp_obj.connect(mail_host, 25)  # 25 为 SMTP 端口号
            smtp_obj.login(mail_user, mail_pass)
            smtp_obj.sendmail(sender, receivers, message.as_string())
            print('邮件发送成功,请查收')
            self.log.info('邮件发送成功')
        except Exception as e:
            print(e)
            print('无法发送邮件')
            self.log.error('无法发送邮件')


if __name__ == '__main__':
    mail = SendMail()
    mail.send_mail()

说明:

  • 设置邮件内容,邮件格式等

连接数据库:

"""
连接数据库
"""

from Common.Utils import Utils
from Conf import Config
from Common import Log
import pymysql
import traceback


class Database(Utils):
    def __init__(self):
        super().__init__()
        self.config = Config.Read_config()
        self.log = Log.MyLog()
        self.host = self.config.get_database('host')
        self.user = self.config.get_database('user')
        self.password = self.config.get_database('password')
        self.db = self.config.get_database('db')
        self.port = int(self.config.get_database('port'))

    def connect_db(self):
        """
        连接数据库
        :return:
        """
        cur = ''
        try:
            conn = pymysql.connect(
                host=self.host,
                user=self.user,
                port=self.port,
                password=self.password,
                db=self.db,
                charset='utf8'
            )

            cur = conn.cursor()
        except Exception:
            print('Fail to connect the database')
            print(traceback.format_exc())
            self.log.error(traceback.format_exc())
        return cur

    def fetch_data_from_db(self, table_name, param, condition):
        """
        查询数据库并返回所有结果记录
        :param table_name: 表名称
        :param param: 需要获取的字段名称
        :param condition: 需填写完整的查询语句,可为空字符;注意填写的参数类型(str, int)准确填写
        :return:
        """
        cur = self.connect_db()
        list_dic_data = []

        # 执行查询语句
        sql_s = 'select ' + param + ' from ' + table_name + ' ' + condition
        print(sql_s)

        # 返回记录数量
        res = cur.execute(sql_s)
        print(res)

        # 获取表字段名和表数据
        table_param = [item[0] for item in cur.description]
        all_records = cur.fetchall()

        # 断开数据库连接
        cur.close()

        # 将数据组成一个dict
        for i in all_records:
            dic_data = {}
            for j in range(len(table_param)):
                dic_data[table_param[j]] = i[j]
            list_dic_data.append(dic_data)

        return list_dic_data


if __name__ == '__main__':
    d = Database()
    c = d.connect_db()
    sql_sentence = 'select * from t_user '

    results = c.execute(sql_sentence)
    print(results)
    print(c.description)
    print(d.fetch_data_from_db('t_user','*','where mobile = \'11111111111\''))

说明:

  • 连接数据库
  • 封装方法:查询数据库的数据

设置运行用例前和运行用例后的操作

创建 environment.properties 文件:
r在这里插入图片描述
创建 Properties.py 文件:

"""
编辑/读取/添加参数到 环境配置文件(allure报告中的环境参数设定)

"""


import re
import os
import tempfile
from Common.Log import MyLog


class Properties:

    def __init__(self, file_name):
        self.file_name = file_name
        self.properties = {}

        try:
            with open(self.file_name, 'r') as f:
                for line in f:
                    line = line.strip()
                    if line.find('=') > 0 and not line.startswith('#'):
                        strs = line.split('=')
                        self.properties[strs[0].strip()] = strs[1].strip()
        except Exception as e:
            MyLog.error('something is wrong!!!! when read the file ' + file_name)
            raise e

    def has_key(self, key):
        return key in self.properties

    def get(self, key, default_value=''):
        if key in self.properties:
            return self.properties[key]
        return default_value

    def put(self, key, value):
        self.properties[key] = value
        replace_property(self.file_name, key + '=.*', key + '=' + value, True)


def replace_property(file_name, from_regex, to_str, append_on_not_exists=True):
    tmpfile = tempfile.TemporaryFile()

    if os.path.exists(file_name):
        with open(file_name, 'r') as r_open:
            pattern = re.compile(r'' + from_regex)
            found = None
            for line in r_open:
                if pattern.search(line) and not line.strip().startswith('#'):
                    found = True
                    line = re.sub(from_regex, to_str, line)
                tmpfile.write(line.encode())
            if not found and append_on_not_exists:
                tmpfile.write(('\n' + to_str).encode())
        tmpfile.seek(0)

        content = tmpfile.read()

        if os.path.exists(file_name):
            os.remove(file_name)

        with open(file_name, 'wb') as f_w:
            f_w.write(content)

        tmpfile.close()
    else:
        print("file %s not found" % file_name)


if __name__ == "__main__":
    file_path = '../environment.properties'
    props = Properties(file_path)  # 读取文件
    props.put('www', '111111')  # 修改/添加key=value
    print(props.get('Author'))  # 根据key读取value

创建 conftest.py 文件(该文件的名称是特定的,不可随便更改,pytest会自动去检测这个文件)

from Common.Utils import *
import os
import pytest
from Common.Email import SendMail
from Common.Log import MyLog
from Common.Properties import Properties
from Conf.Config import Read_config
import allure
"""
该文件名称不可修改!
目前单独运行某个case时,会先调用以下方法
"""

current_path = os.path.abspath(os.path.dirname(__file__))
# rootPath = os.path.split(current_path)[0]

fail_case_txt_path = 'fail_cases.txt'
err_mail_log_path = current_path + '/Log/err_mail.log'

properties = Properties(current_path + '/environment.properties')
config = Read_config()


@pytest.fixture(scope='package', autouse=True)
def resource():
    """
    这是pytest的装饰器
    package是module级别的,执行多个.py文件时,只执行一次该方法
    yield前面的内容 是运行前执行的,相当于testNG的setup
    yield后面的内容,是运行后执行的,相当于testNG的teardown
    :return:
    """

    # 清除上次运行的失败case的记录
    # 清除上此运行的错误日志(仅用于发送个邮箱的日志)
    Utils.clean_file_content(fail_case_txt_path)
    Utils.clean_file_content(err_mail_log_path)

    current_env = config.get_global('current_environment')
    properties.put('Environment', current_env)
    properties.put('Endpoint', config.get_conf(current_env, 'endpoint'))
    properties.put('Author', config.get_conf(current_env, 'tester'))
    properties.put('Version', config.get_conf(current_env, 'version_code'))


@pytest.fixture(scope='package', autouse=True)
def send_mail():
    yield
    if os.path.getsize(err_mail_log_path) != 0:
        try:
            mail = SendMail()
            mail.send_mail()
        except Exception:
            MyLog.error('发送邮件失败,请检查邮件配置')
            raise


@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item):
    """
    pytest_runtest_makereport 钩子方法

    when=’setup’ 返回setup 的执行结果
    when=’call’ 返回call 的执行结果
    when=’teardown’返回teardown 的执行结果

    :param item: 运行的case的对象
    :return:
    """
    outcome = yield
    rep = outcome.get_result()
    setattr(item, "rep_" + rep.when, rep)
    if rep.when == 'call':
        if rep.failed:
            # 如果case运行失败,则获取case的名称并写入一个txt文件中
            Log.MyLog.error('test case: ' + item.name + '--------' + rep.outcome)
            with open(fail_case_txt_path, "a+") as f:
                f.write(item.name + '\n')
            print('\n%s' % item.name + 'is ' + rep.outcome)

说明:

  • 设置运行用例前的操作
  • 设置运行用例后的操作
  • 该段代码主要是:1. 运行用例前,设置测试报告的一些内容;2. 运行用例时,如果有失败用例,写入一个指定文件中;3. 若有失败用例,则有邮件通知
  • 19
    点赞
  • 115
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值