Python + Requests + pytest自动化接口测试框架集成

本文参考:https://my.oschina.net/u/3041656/blog/820023
公司在做中台管理系统,考虑到数据测试比较多,就打算做接口测试。请教过朋友,他们主要使用jmeter做接口测试,本来我也想使用jmeter做测试,但考虑到jmeter自动化需要懂java才能做二次封装,坦白了我不会java,还没学哈哈哈,然后就用了我唯一会的python找了一些资料参考封装了一个,其实也不算封装,东凑凑西凑凑就出来,今天刚写好,发出来大家给点意见,毕竟集思广益嘛

项目目录
在这里插入图片描述
不废话,一一介绍各个py是干啥的

common.py文件里面封装一些公共方法,读xlsx文件和xml文件以及读取yaml文件


```python
import os
import xml.etree.ElementTree as ET
import yaml
from xlrd import open_workbook


class Common:
    def __init__(self):
        """ 指定文件的路径 """
        self.rootPath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

    def get_xls(self, xls_name, sheet_name):
        """
        获取xls文件
        :param xls_name: 文件名
        :param sheet_name:
        :return:
        """
        cls = []
        # get xls file's path
        xlsPath = os.path.join(self.rootPath, "testFile", 'case', xls_name)
        # open xls file
        file = open_workbook(xlsPath)
        # get sheet by name
        sheet = file.sheet_by_name(sheet_name)
        # get one sheet's rows
        nrows = sheet.nrows
        for i in range(nrows):
            if sheet.row_values(i)[0] != u'case_name':
                cls.append(sheet.row_values(i))
        return cls

    def get_yaml(self, funcname, yaml_file):
        """ 读取yaml里面的数据"""
        with open(os.path.join(self.rootPath + "/Data/", yaml_file), "r", encoding='utf8') as f:
            data = yaml.load(f, Loader=yaml.Loader)
            tmpdata = data[funcname]
            return tmpdata

    def get_xml_sql(self, filename, sql_id):
        """
        get sql xml
        :return:
        """
        sql_path = os.path.join(self.rootPath, "testFile/xmlSql", filename)
        tree = ET.parse(sql_path)
        root = tree.getroot()
        sql = {}
        for data in root.findall('sql'):
            sql_id_xml = data.get("id")
            if sql_id_xml == sql_id:
                sql[sql_id_xml] = data.text
        sql = sql.get(sql_id)
        return sql


common = Common()

config读取配置文件,我的配置文件写在yaml文件中管理,放在Data文件夹中


```python
# coding=utf-8
"""
读取全局配置
"""
from Common.common import common

# 配置环境

version = common.get_yaml("version", "config.yaml")


"""
配置数据
"""
ApiUrl = common.get_yaml(version, "config.yaml")['ApiUrl']
VerificationCode = common.get_yaml(version, "config.yaml")['VerificationCode']

config.yaml

version: test

test:
  ApiUrl: http://localhost:3000/
  VerificationCode: '8888'
  database:
    host: "192.168.1.90"
    username: "root"
    password: ""
    port: 2019

configDB连接数据库用的

import pymysql
from Common import Log
from Common.config import version
from Common.common import common


class MyDB:
    def __init__(self):
        self.logger = Log.MyLog()
        database = common.get_yaml(version, "config.yaml")['database']
        self.host = database['host']
        self.port = database['port']
        self.user = database['username']
        self.password = database['password']
        self.db = None
        self.cursor = None

    def connect_db(self):
        """
        connect to database
        :return:
        """
        try:
            # connect to DB
            self.db = pymysql.connect(host=self.host, port=self.port, user=self.user, passwd=self.password, charset='utf8')
            # create cursor
            self.cursor = self.db.cursor()
            print("Connect DB successfully!")
            self.logger.info("Connect DB successfully!")
        except ConnectionError as ex:
            self.logger.error(str(ex))
            print(ex)

    def execute_sql(self, sql):
        """
        execute sql
        :param sql:
        :return:
        """
        # executing sql
        self.cursor.execute(sql)
        return self.cursor

    @staticmethod
    def get_all(cursor):
        """
        get all result after execute sql
        :param cursor:
        :return:
        """
        value = cursor.fetchall()
        return value

    @staticmethod
    def get_one(cursor):
        """
        get one result after execute sql
        :param cursor:
        :return:
        """
        value = cursor.fetchone()
        return value

    def close_db(self):
        """
        close database
        :return:
        """
        self.db.close()
        print("Database closed!")
        self.logger.info("Database closed!")


MyDB = MyDB()

HttpRequests.py发送接口请求,先设置接口地址,请求方式,请求参数,最后调用send_requests发送请求

import json
import requests

from Common.Log import MyLog
from Common.config import ApiUrl


class HttpRequests(object):
    """
    eg: request = HttpRequests()
        response = request(method, url, data)
        or
        response = request.send_request(method, url, data)
        print(response.text)()
        self.headers = {}
        self.params = {}
    """
    def __init__(self):
        self.session = requests.Session()
        self.data = None
        self.params = {}
        self.url = None
        self.method = None
        self.files = {}
        self.state = 0

    def set_url(self, url_path):
        """
        set url
        :param: interface url
        :return:
        """
        self.url = ApiUrl + url_path

    def set_method(self, method):
        """
        set method
        :param method:
        :return:
        """
        self.method = method

    def set_headers(self, header):
        """
        set headers
        :param header:
        :return:
        """
        self.headers = header

    def set_params(self, param):
        """
        set params
        :param param:
        :return:
        """
        self.params = param

    def set_data(self, data):
        """
        set data
        :param data:
        :return:
        """
        self.data = str(data)

    def set_files(self, filename):
        """
        set upload files
        :param filename:
        :return:
        """
        if filename != '':
            file_path = 'F:/AppTest/Test/interfaceTest/testFile/img/' + filename
            self.files = {'file': open(file_path, 'rb')}

        if filename == '' or filename is None:
            self.state = 1

    def send_request(self, params_type='json', **kwargs):
        """
        发送接口请求
        :param params_type: 默认参数类型json格式
        :param kwargs:
        :return:
        """
        method = self.method.upper()
        params_type = params_type.upper()
        if isinstance(self.data, str):
            try:
                self.data = json.loads(self.data)
            except Exception as e:
                self.data = eval(self.data)
                MyLog.info(e)
        if 'GET' == method:
            response = self.session.request(method=method, url=self.url, params=self.params, **kwargs)
        elif 'POST' == method:
            if params_type == 'FORM':  # 发送表单数据,使用data参数传递
                response = self.session.request(method=method, url=self.url, data=self.data, **kwargs)
            elif params_type == 'JSON':  # 如果接口支持application/json类型,则使用json参数传递
                response = self.session.request(method=method, url=self.url, json=self.data, **kwargs)
            else:  # 如果接口需要传递其他类型的数据比如 上传文件,调用下面的请求方法
                response = self.session.request(method=method, url=self.url, **kwargs)
        # 如果请求方式非 get 和post 会报错,当然你也可以继续添加其他的请求方法
        else:
            raise ValueError('request method "{}" error ! please check'.format(method))
        return response

    def close_session(self):
        self.session.close()
        try:
            del self.session.cookies['JSESSIONID']
        except Exception as e:
            MyLog.info(e)
            raise e


HttpRequests = HttpRequests()

最后common文件只剩下Log日志封装

# -*- coding: utf-8 -*-

"""
封装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')
        log_meg = str(log_meg)
        logger.debug("[DEBUG " + get_current_time() + "]" + log_meg)
        remove_handler('debug')

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

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

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

    @staticmethod
    def critical(log_meg):
        set_handler('critical')
        log_meg = str(log_meg)
        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")


run.py运行文件, 运行不起来可能是你没安装allure报告,allure安装包: https://pan.baidu.com/s/19eohM2P6VNWCaJETIb93Aw, 提取码: 1xcc

import os
import subprocess
import time
import pytest
from Common.Log import MyLog

PATH = os.path.split(os.path.realpath(__file__))[0]
xml_report_path = PATH + "/report/xml"
html_report_path = PATH + "/report/html"
tm = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(time.time()))


def invoke(md):
    output, errors = subprocess.Popen(md, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
    o = output.decode("utf-8")
    return o


if __name__ == '__main__':
    MyLog.info("======================" + tm + "======================")
    args = ['-s', '-q', '--alluredir', xml_report_path]
    pytest.main(args)
    cmd = 'allure generate %s -o %s --clean' % (xml_report_path, html_report_path)
    invoke(cmd)

testFile里面放xlsx数据,看一下文件里面内容,注意纯数字需要把单元格式修改成文本,不然读出来会在数字后增加2.0, 还有读不出来可能是你的插件版本不对,按requirements.txt文件里的版本来就对了
在这里插入图片描述
**最后重点讲一下测试用例, setup_class和teardown_class在这个测试类运行之前连接数据库和运行完成关闭数据库连接,具体的使用方法可以查资料,pytest.mark.parametrize就是参数化,参数名要与xlsx文件中对应哦,pytest.fixture这个装饰器我也是刚了解不久,目前用来装饰sql查询,然后可以在测试用例中直接把函数名当参数传进去,是不是很方便,哈哈 **

import pytest

from Common.configDB import MyDB
from Common.HttpRequests import HttpRequests
from Common.Log import MyLog
from Common.config import *

login_xls = common.get_xls("userCase.xlsx", "login")


class TestLogin:
    def setup_class(self):
        MyDB.connect_db()

    def teardown_class(self):
        MyDB.close_db()

    @pytest.mark.parametrize("case_name, url_path, method, phone, password, result, code, msg", login_xls)
    def test_login(self, case_name, url_path, method, phone, password, result, code, msg, database_data):
        # 设置url
        HttpRequests.set_url(url_path)
        # 设置请求方式
        HttpRequests.set_method(method)
        # 设置请求参数
        data = {"code": VerificationCode, "key": "", "loginphone": phone, "password": password}
        HttpRequests.set_data(data)
        # 发送请求
        self.response = HttpRequests.send_request()
        # 检查结果
        self.check_result(msg, code, result, database_data)

        MyLog.info("{}测试结束".format(case_name))

    def check_result(self, msg, code, result, database_data):
        response_json = self.response.json()
        print(type(result))
        if result == '0':
            assert response_json['msg'] == msg
            assert response_json['code'] == int(code)
            assert response_json['data'] == database_data
        elif result == '1':
            assert response_json['msg'] == msg
            assert response_json['code'] == int(code)

    @pytest.fixture()
    def database_data(self):
        sql = common.get_xml_sql('login.xml', 'queryUser')
        data = MyDB.execute_sql(sql).fetchall()
        return data

写到最后有一个遗留问题,数据库查询出来的数据和接口返回的数据做对比很麻烦,我现在只写了一条数据对比,实际项目中可能有很多数据都需要对比,一一对比感觉很麻烦,有没有哪位大神知道怎么写简单些

补充一下运行环境

allure-pytest==2.8.36
allure-python-commons==2.8.36
atomicwrites==1.4.0
attrs==20.3.0
certifi==2020.12.5
chardet==4.0.0
colorama==0.4.4
idna==2.10
importlib-metadata==3.7.0
iniconfig==1.1.1
numpy==1.19.5
packaging==20.9
pluggy==0.13.1
py==1.10.0
PyMySQL==0.9.3
pyparsing==2.4.7
pytest==6.2.2
pytest-repeat==0.9.1
pytest-rerunfailures==9.1.1
PyYAML==5.4.1
requests==2.25.1
six==1.15.0
toml==0.10.2
typing-extensions==3.7.4.3
urllib3==1.26.3
xlrd==1.2.0
zipp==3.4.1

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值