基于python+requests+pytest实现接口自动化测试

框架介绍

基于python+requests+pytest实现接口自动化测试_第1张图片

该框架是基于OpenAPI接口自动化测试的理论搭建,基于python+requests+pytest实现。通过封装主要核心方法,在一定的基础上增强代码后期的可维护性和易读性。

  • common:存放一些封装的公共的方法,例如断言、日志、读取测试用例数据的方法

  • data:存放测试数据

  • reports:存放生成的测试报告

  • test_OpenAPI:存放用例以及接口封装

    page_api:按照对象对接口进行封装
    test_cases:测试用例

  • utils:存放封装的工具,例如读取文件、发送接口请求等

  • base_url:测试的接口url地址

  • conftest:存放获取token的方法

  • get_token:存放获取token时需要使用的数据

  • main:运行测试用例的py文件

  • pytest.ini:pytest的配置文件

conftest文件

import pytest

from common.logger import Logger
from utils.read_yaml import read_yaml
from utils.requests_utils import RequestUtils


@pytest.fixture(scope="session")
def get_headers():
    try:
        # 读取获取token需要用到的数据
        data_token = read_yaml("./get_token.yml")
        # 获取token
        response = RequestUtils().send_request(url=data_token["url"], method=data_token["method"],
                                               json=data_token["json"])

        token_value = response.json()["access_token"]
        Logger.logger_in().info('token获取成功,token值为:{}'.format(token_value))
        headers = {
            "Authorization": "Bearer" + ' ' + token_value,
            "Host": "openapi.italent.cn",
            "Content-Type": "application/json"
        }

        return headers
    except Exception as e:
        # print(repr(e))
        Logger.logger_in().error('获取token失败,原因为{}'.format(repr(e)))

我们知道,在pytest中,conftest文件会在所有的测试用例执行之前运行,常常用于每个接口用例需要使用到共同的token、需要共用测试用例的数据、每个接口用例需要共用配置等等。

在本框架中,我将token的获取设计在conftest中:通过读取yaml文件,将获取token所需要的参数拿到;获取token后,将token保存在headers中,并返回,便于我们在进行接口请求时,直接使用含有token的请求头。并将token的获取过程生成日志,便于后期定位问题。

common模块介绍

获取断言

class Assert:
    #   实现断言,并且将断言的结果写入到日志中

    @staticmethod
    def code_assert(response, assert_type,assert_msg):
        if assert_type == "code":
            try:
                assert response.status_code == assert_msg
                Logger.logger_in().info('断言成功!')
            except Exception as e:
                Logger.logger_in().error('断言失败!原因是:{}'.format(repr(e)))
            # print(response.text)
        else:
            print("请输入code")

        if assert_type == "massage":
            try:
                assert response.text == assert_msg
                Logger.logger_in().info('断言成功!')
            except Exception as e:
                Logger.logger_in().error('断言失败!原因是:{}'.format(repr(e)))
                # print(response.text)
            else:
                print("请输入massage")

通过封装Assert类,实现断言的效果,并且将断言的结果记录到日志中。

使用该方法时,需要传入接口响应体、想要断言的具体内容以及预期结果,在本框架中主要考虑的是对响应的状态码以及响应体信息进行断言;想要对状态进行断言则输入“code”,想要对响应体信息进行断言,则输入"massage";若获取到的响应中对应的信息与预期结果相等时,则断言成功。

因为可能存在断言失败的情况,所以使用了python的异常处理机制,当发生异常时,会对异常进行捕获,最后通过 Logger类中的logger_in()方法,将断言结果写入到日志中。

后续优化为:

# -*- coding: gbk -*-
from common.logger import Logger


class Assert:
    #   实现断言,并且将断言的结果写入到日志中
    @staticmethod
    def custom_assert(response_assert,assert_msg):
        try:
            assert response_assert == assert_msg
            Logger.logger_in().info('断言成功!')
        except Exception as e:
            Logger.logger_in().error('断言失败!原因是:{}'.format(repr(e)))

考虑到仅上述方法的局限性(只能通过code以及massage来做断言),将断言部分进行了优化,传入参与断言的response_assert,与预期结果assert_msg进行比对,更加灵活。

读取测试数据

class GetData:

    @staticmethod
    def get_data(filename):
        data_names = re.findall(r'_(.*)\.', filename)
        data_name = str(data_names[0]) + '.xlsx'
        path = os.path.join('./data/', data_name)
        excel_cases = ExcelHandler(path)
        data_cases = excel_cases.read_excel("Sheet1")
        print(data_cases)
        return data_cases

之前的数据读取都是直接写在用例里面的,通过调用 ExcelHandler类中的read_excel方法来读取测试数据,后期考虑到设计的话存在的一个问题:当我们用例数增多时,如果文件路径发生了改变,那么要修改每一个测试用例中的调用路径,这样的成本是巨大的,不利于后期维护。

为了解决上述问题,我封装了一个GetData类。通过filename传入参数,使用正则表达式,截取出filename中_后的关键字,使用os.path.join()方法将路径和文件名拼接在一起,封装一个静态方法,方便在每个用例处调用直接调用,减少不必要的内存占用和性能消耗。但这里也带来一个限制,那就是测试用例的文件名_ 后的文字必须于测试数据的文件名保持一致。

这样封装的话,后期如果文件路径发生了变化,我们只需要修改类中的的路径即可。

日志模块

class Logger:
    __logger = None

    @classmethod
    def logger_in(cls):
        if cls.__logger is None:
            # 创建日志器
            cls.__logger = logging.getLogger("APIlogger")
            cls.__logger.setLevel(logging.DEBUG)
        # 判断是否存在handler,不然每次都会新建一个handler,导致日志重复输出
        if not cls.__logger.handlers:
            # 获取当前日期为文件名,年份最后2位+月份+日期
            file_name = str(datetime.datetime.now().strftime('%g' + '%m' + "%d")) + '.log'
            # 创建处理器
            handler = logging.FileHandler(os.path.join('./log', file_name))
            # handler = logging.StreamHandler()
            # 创建格式器
            formatter = logging.Formatter('%(asctime)s [%(filename)s:%(lineno)d] %(levelname)s  %(message)s',
                                          '%Y-%m-%d %H:%M:%S')
            cls.__logger.addHandler(handler)
            handler.setFormatter(formatter)
        return cls.__logger

在这里使用了单例设计模式,在其他用户调用该方法时,首先要判断是否已经生成了日志器,如果已经生成,那么就直接返回,如果没有,就生成一个新的日志器,这样做是为了避免每次调用时都生成一个新的日志器,方便对实例个数的控制并节约系统资源。

前期实现日志写入时,并没有判断是否只存在一个处理器(handle),这也就导致每次写入日志时,信息都会在上一次写入的次数上递增一次,导致日志的重复输出。效果如下:

2023-07-27 21:44:59 [requests_utils.py:24] ERROR  接口请求失败,原因为:ConnectionError(ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')))
2023-07-27 21:44:59 [conftest.py:29] ERROR  获取token失败,原因为AttributeError("'NoneType' object has no attribute 'json'")
2023-07-27 21:44:59 [conftest.py:29] ERROR  获取token失败,原因为AttributeError("'NoneType' object has no attribute 'json'")
2023-07-27 21:44:59 [wrappers.py:16] INFO  test_create_offer开始执行
2023-07-27 21:44:59 [wrappers.py:16] INFO  test_create_offer开始执行
2023-07-27 21:44:59 [wrappers.py:16] INFO  test_create_offer开始执行
2023-07-27 21:44:59 [requests_utils.py:21] INFO  接口请求成功,响应值为:{"message":"Authorization header is empty"}

2023-07-27 21:44:59 [requests_utils.py:21] INFO  接口请求成功,响应值为:{"message":"Authorization header is empty"}

2023-07-27 21:44:59 [requests_utils.py:21] INFO  接口请求成功,响应值为:{"message":"Authorization header is empty"}

2023-07-27 21:44:59 [requests_utils.py:21] INFO  接口请求成功,响应值为:{"message":"Authorization header is empty"}

2023-07-27 21:44:59 [assert_cases.py:15] ERROR  断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR  断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR  断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR  断言失败!原因是:AssertionError()
2023-07-27 21:44:59 [assert_cases.py:15] ERROR  断言失败!原因是:AssertionError()

为了避免这种情况,使用if对是否存在handle进行判断,如果已经存在日志处理器了,那么就直接返回即可,不用再重新生成。

测试用例的日志输入

def write_case_log(func):
    """
    记录用例运行日志
    :param func:
    :return:
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        Logger.logger_in().info('{}开始执行'.format(func.__name__))
        func(*args, **kwargs)
        Logger.logger_in().info('{}执行完毕'.format(func.__name__))
    return wrapper

考虑后期可能存在大量的测试用例,如果在每个测试用例中都分别加入日志输入,那么会耗费大量的精力且后期不易维护;为了便于后期的代码维护,将用例的日志输入使用装饰器实现。

utils模块介绍

读取测试数据方法封装

class ExcelHandler:
    def __init__(self, path):
        self.path = path

    def open_excel(self, sheet_name):
        #  读取excel文件
        wb = openpyxl.load_workbook(self.path)
        # 获取sheet
        sheet = wb[sheet_name]
        wb.close()
        return sheet

    def get_header(self, sheet_name):
        # 获取表头
        sheet = self.open_excel(sheet_name)
        header = []
        # 遍历第一行,获取表头
        for i in sheet[1]:
            header_value = i.value
            header.append(header_value)
        return header

    def read_excel(self, sheet_name):
        sheet = self.open_excel(sheet_name)  # 获取sheet
        rows = list(sheet.rows)  # 将sheet里面的每一行转换成列表,方便进行遍历
        data = []
        for row in rows[1:]:
            row_data = []
            for cell in row:
                row_data.append(cell.value)
            data_dict = dict(zip(self.get_header(sheet_name), row_data))
            # print(data_dict)  # 将每一行的值与表头关联起来
            data.append(data_dict)

        return data

通过使用python的openpyxl库实现excel表中测试数据的读取。首先读取并返回指定的sheet,再获取到表头,最后通过使用dict()方法将表头与数据关联起来存在字典中,方便实现接口参数的传入。

yaml文件的读取

# -*- coding: gbk -*-
import yaml


def get_baseurl(path, baseurl, keyurl):

    with open(path, "r", encoding="utf8") as f:
        yaml_url = yaml.load(f, Loader=yaml.FullLoader)
        print(yaml_url)
        f.close()
    return yaml_url[baseurl][keyurl]


def read_yaml(path):

    with open(path, "r", encoding="utf8") as f:
        yaml_ob = yaml.load(f, Loader=yaml.FullLoader)
        print(yaml_ob)
        f.close()
    return yaml_ob

使用yaml文件,主要是为了存放获取token的数据以及测试接口所需的url地址,便于我们在测试时进行环境的切换。

首先定义get_baseurl()方法获取环境url,通过输入baseurl, keyurl来获取对应环境的url地址。
然后定义read_yaml()方法获取yaml文件中的信息,在这里将获取token所需要的数据放在yaml文件中,也是为了后期维护使用时更加方便(可以直接在pycharm中进行维护,且十分易读,结构分明)。

统一接口请求封装

class RequestUtils:
    session = requests.session()

    def __init__(self):
        self.get_url = get_baseurl("./base_url.yml", "base", "url")  # 读取不同环境的url,方便切换测试环境和线上环境

        def send_request(self, url, method, **kwargs):
        try:
            url_1 = self.get_url + url
            # print(self.get_url)
            # print(url_1)
            Logger.logger_in().info('-----------------{}接口开始执行-----------------'.format(url))
            response = RequestUtils.session.request(url=url_1, method=method, **kwargs)
            Logger.logger_in().info('接口请求成功,响应值为:{}'.format(response.text))
            return response
        except Exception as e:
            Logger.logger_in().error('接口请求失败,原因为:{}'.format(repr(e)))
            return e

首先是使用了requests模块的session(),帮我们实现状态保持。(虽然目前并没有用到这样的场景,但为了框架的完整性,依旧使用了该方法,在后续涉及到需要进行状态保持的接口关联时,可以直接使用)。

定义send_request()方法,实现接口url的拼接以及接口请求的发送,并且将接口请求的情况记录到日志中。

在此处新增了一行日志输出的分隔线,由于后期实现了业务接口关联调用,一个测试用例会执行多个接口,输出分割线可以帮助我们更好的阅读日志。

2023-07-30 13:35:38 [wrappers.py:16] INFO  test_create_offer开始执行
2023-07-30 13:35:38 [requests_utils.py:20] INFO  -----------------/RecruitV6/api/v1/Applicant/GetApplicantIds接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO  接口请求成功,响应值为:{"data":["f617a04e-ba25-4e1a-80ae-3d551e633795"],"code":200,"message":""}
2023-07-30 13:35:38 [requests_utils.py:20] INFO  -----------------/dataservice/api/DataSource/GetDataSource接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO  接口请求成功,响应值为:{"data":[{"key":"RecruitOnBoarding.OfferType","dataSourceResults":[{"text":"社招Offer","value":"ab56afa5-b35d-4350-8fd3-1d15eef845d4","textJson":"社招Offer","isActive":"1","isSystem":false},{"text":"校招Offer","value":"c779cf3d-28d1-48f9-989d-7396b04b1a90","textJson":"校招Offer","isActive":"1","isSystem":false},{"text":"实习offer","value":"b1fdc10d-a1fc-4be1-ba0d-32258654749a","textJson":"实习offer","isActive":"1","isSystem":false},{"text":"自定义offer1","value":"04aef502-1ac1-488d-9e4e-b874192c4a7c","textJson":"自定义offer1","isActive":"1","isSystem":false},{"text":"测试offer类型2","value":"df5826dc-6e78-4cf4-a4fd-fc1be1f88b2e","textJson":"测试offer类型2","isActive":"1","isSystem":false},{"text":"自定义offer2","value":"8690e89b-b4ca-4750-84c4-ad0103966f73","textJson":"自定义offer2","isActive":"1","isSystem":false},{"text":"测试offer类型1","value":"d54ae3fe-997a-4271-8851-33c4205b8046","textJson":"测试offer类型1","isActive":"1","isSystem":false}],"fieldName":null,"dataSourceType":8}],"code":"200","message":""}
2023-07-30 13:35:38 [requests_utils.py:20] INFO  -----------------/RecruitV6/api/v1/Apply/GetApplyListByApplicantId接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO  接口请求成功,响应值为:{"data":[{"applyId":"be4a15e9-97a2-4904-9cc1-160ae23c4ee1","applicantId":"f617a04e-ba25-4e1a-80ae-3d551e633795","jobId":"f70c0c9a-a4c6-4cb0-a6f0-12efbc51f2b5","processId":"d5d17dbf-814d-4f32-9cad-c76339ab3293","processPhaseId":"1f126dbf-f4e8-4e56-a7bf-bfac1426a793","processStatusId":"e15f413f-d0c0-47d4-a2ea-ff034278f6cc","fieldValues":{}}],"code":200,"message":""}
2023-07-30 13:35:38 [requests_utils.py:20] INFO  -----------------/RecruitV6/api/v1/RecruitOnBoarding/CreateOfferInfo接口开始执行-----------------
2023-07-30 13:35:38 [requests_utils.py:22] INFO  接口请求成功,响应值为:{"data":"5be1ef4b-7eb6-4293-a0d4-1320788349b7","code":200,"message":null}
2023-07-30 13:35:38 [requests_utils.py:20] INFO  -----------------/RecruitV6/api/v1/RecruitOnBoarding/GetOfferInfos接口开始执行-----------------
2023-07-30 13:35:39 [requests_utils.py:22] INFO  接口请求成功,响应值为:{"data":[{"id":"5be1ef4b-7eb6-4293-a0d4-1320788349b7","createBy":100,"createdTime":"2023-07-30T13:35:39.1124553","post":null,"postObj":null,"needDepartment":null,"applyChannel":null,"personFrom":null,"applyDate":"2023-05-23T13:58:34.5342975","isComplete":false,"applicantElink":null,"offerState":0,"sendApprovalTime":null,"sendOfferTime":null,"approvalState":0,"orgFullName":"","offerTypeId":"ab56afa5-b35d-4350-8fd3-1d15eef845d4","recordCreated":null,"applicantId":"f617a04e-ba25-4e1a-80ae-3d551e633795","applyId":"be4a15e9-97a2-4904-9cc1-160ae23c4ee1","modifiedBy":100,"modifiedTime":"2023-07-30T13:35:39.1124553","name":"test02","email":null,"phone":null,"phonetype":null,"idType":null,"idNumber":null,"org":null,"orgObj":null,"planDate":null,"jobId":"f70c0c9a-a4c6-4cb0-a6f0-12efbc51f2b5","jobCode":"J16571","proLine":null,"reasonStr":null,"pactTime":null,"JobGrade":null,"jobGradeObj":null,"JobRank":null,"jobRankObj":null,"probationDate":null,"probation":null,"beforeBasicPay":null,"afterBasicPay":null,"publicBasic":null,"securityBasic":null,"effectiveDate":null,"endDate":null,"signDate":null,"attachments":null,"approvalMaterials":null,"refuseReasonId":null,"acceptDate":null,"rejectDate":null,"welfare":null,"gender":null,"entryDate":null,"pdfFile":null,"signingThirdPartyDate":null,"internShipSubsidy":null,"lineManage":0,"lineManagerObj":null,"dotManage":0,"dotManagerObj":null,"beginWorkDate":null,"workPlace":null,"recruitRequirementLookup":null,"permissionField":null,"extendInfos":[{"text":"","name":"Onwer","value":100},{"text":"","name":"extssdfgfghhj4356455_410000_1950780832","value":null},{"text":"","name":"extextznjyf606773839295707_410000_1316167470","value":null},{"text":"","name":"extdsf54343535453_410000_401361418","value":null},{"text":null,"name":"exte3434_410000_432447782","value":null},{"text":null,"name":"extzhuanzhenghougangweigongzi_410000_1381981686","value":null},{"text":null,"name":"extzonghegongshijiabanbutieshiyongqi_410000_740884613","value":null},{"text":null,"name":"extyuexinzongjishiyongqi_410000_460566386","value":null},{"text":"","name":"extpicUpload_410000_716039143","value":null},{"text":null,"name":"extyuebiaozhungongzizhuanzheng_410000_1200505899","value":null},{"text":"","name":"extdsf43543_410000_375238909","value":null},{"text":null,"name":"extccshikaifa_410000_1584751792","value":null},{"text":null,"name":"extjixiaogongzizhuanzheng_410000_949177300","value":null},{"text":null,"name":"extxinzo_410000_568905541","value":null},{"text":"","name":"extasiug_410000_1417512741","value":null},{"text":"","name":"extshifoulanlinggongrne_410000_35014856","value":null},{"text":null,"name":"extlookup11_410000_218776234","value":null},{"text":"","name":"extaaa_410000_916735768","value":null},{"text":"","name":"extextdebje6067731259604019_410000_519697908","value":null},{"text":null,"name":"extextrzdsydybff6067731961161106_410000_518818249","value":null},{"text":null,"name":"extjixiaogongzishiyongqi_410000_1510979931","value":null},{"text":null,"name":"extextffbs6067731166350017_410000_511507282","value":null},{"text":null,"name":"extyuexinzongjizhuanzheng_410000_1981787164","value":null},{"text":"","name":"extavc_410000_1509548279","value":null},{"text":"","name":"extfileUpload_410000_545164225","value":null},{"text":"","name":"extextcqjl6067731996166332_410000_2014021631","value":null},{"text":null,"name":"extzonghegongshijiabanbutiezhuanzheng_410000_691705984","value":null},{"text":"","name":"extfdgdfgdfg43345_410000_1276538596","value":null},{"text":null,"name":"extgangweigongzizhuanzheng_410000_1001575836","value":null},{"text":null,"name":"extcustmult01_410000_1805096460","value":null},{"text":"","name":"extextzgdlj60677317683_410000_1339661530","value":null},{"text":"","name":"extextsfyzfbt606773224919108_410000_1532692855","value":null},{"text":"","name":"extextsfyznjyf60677353100989_410000_622433298","value":null},{"text":"","name":"extextqzf606773631522154_410000_617219108","value":null},{"text":"","name":"extmultiPicUpload_410000_1745111356","value":null},{"text":null,"name":"extjintie234_410000_438160583","value":null},{"text":"","name":"extextsfycqjl606773667001049_410000_399093849","value":null},{"text":null,"name":"extdfg234_410000_696428294","value":null},{"text":null,"name":"extextrzdsydebff606773262473866_410000_642512888","value":null},{"text":"","name":"extmultiFileUpload_410000_512503722","value":null},{"text":"","name":"extdsf4535_410000_1974556573","value":null},{"text":null,"name":"extjibengongzizhuanzheng_410000_1864473652","value":null},{"text":"","name":"extextdybje6067731461268998_410000_1909313268","value":null},{"text":"","name":"extextljbz606773376330165_410000_572350980","value":null},{"text":null,"name":"extfuwenben_410000_1787459387","value":null},{"text":null,"name":"extzonghe1232112_410000_2138547005","value":null},{"text":null,"name":"extgangweigongzishiyongqi_410000_1219379","value":null},{"text":"","name":"extdanxuanliebiao_410000_719220132","value":null},{"text":"","name":"extll_410000_1075927860","value":null},{"text":"","name":"extextspr3_410000_790676610","value":null},{"text":"","name":"extsdfds3454_410000_1893554040","value":null},{"text":"","name":"extyuebiaozhungongzi2_410000_1996804789","value":null},{"text":"","name":"extextzfbt6067731799027952_410000_671908912","value":null},{"text":"","name":"extextsfyqzf606773282718948_410000_2041956697","value":null},{"text":null,"name":"extjibengongzishiyongqi_410000_3216026","value":null},{"text":null,"name":"exto0p0_410000_515485633","value":null},{"text":null,"name":"extyuebiaozhungongzishiyongqi_410000_1412658204","value":null},{"text":"","name":"extdfds4356456_410000_2126743478","value":null},{"text":"","name":"extextsfyljbz606773858283801_410000_629856499","value":null},{"text":"","name":"extdsfd435_410000_1216408346","value":null}],"approvalInfos":null,"fileInfos":[{"name":"Attachment","text":"发送附件给应聘者","downloadUrls":[],"clientUrls":[]},{"name":"PdfFile","text":"Offer附件","downloadUrls":[],"clientUrls":[]},{"name":"ApprovalMaterials","text":"发送附件给审批人","downloadUrls":[],"clientUrls":[]},{"name":"extpicUpload_410000_716039143","text":"","downloadUrls":[],"clientUrls":[]},{"name":"extfileUpload_410000_545164225","text":"","downloadUrls":[],"clientUrls":[]},{"name":"extmultiPicUpload_410000_1745111356","text":"","downloadUrls":[],"clientUrls":[]},{"name":"extmultiFileUpload_410000_512503722","text":"","downloadUrls":[],"clientUrls":[]}],"additionalInfos":null}],"code":200,"message":null}
2023-07-30 13:35:39 [assert_cases.py:12] INFO  断言成功!
2023-07-30 13:35:39 [wrappers.py:18] INFO  test_create_offer执行完毕

按照对象对接口进行封装

基于python+requests+pytest实现接口自动化测试_第2张图片
在我们新建offer时,需要使用到applicantid、applyif、offerTypeid,在此处,我分别以接口所属的对象对接口进行封装,将接口封装完毕后,后续我们执行测试用例就调用封装的方法即可。

以offer对象为例做以下说明:

import json
from utils.read_yaml import read_yaml
from utils.requests_utils import RequestUtils


class Offer:

    # 获取offer类型id
    	@staticmethod
        def get_offer_type(path, get_headers):
        data = read_yaml(path)
        # print(json.dumps(header))
        response = RequestUtils().send_request(method=data["method"], url=data["url"], data=json.dumps(data["body"]),
                                               headers=get_headers)
        # print(response.request.body)
        response = json.loads(response.text)
        # print(data['data'][0]["dataSourceResults"][0]["value"])
        # return data['data'][0]["dataSourceResults"][0]["value"]
        return response

    # 创建offer
    @staticmethod
    def create_offer(data, headers, get_offer_type, get_applicant_id, get_apply_id):
        # print(data)
        # print(data["json"])
        # body = json.dumps(data["json"])
        # json.loads(body)
        # 将json的值从字符串转换成字典类型
        body = eval(data["json"])
        body["offerTypeID"] = get_offer_type
        body["applicantId"] = get_applicant_id
        body["applyId"] = get_apply_id
        response = RequestUtils().send_request(url=data["url"], method=data["method"],
                                               json=body, headers=headers)
        response = json.loads(response.text)
        return response

    # 根据offerid查询offer
    @staticmethod
    def get_offer(path, header, offer_id):
        data = read_yaml(path)
        offer_id = str(offer_id)
        body = [offer_id]
        response = RequestUtils().send_request(url=data['url'], method=data["method"], json=body, headers=header)
        return response

在此处,按照offer对象,对获取offer类型、创建offer、查询offer三个OpenAPI接口实现了接口封装。
在我们编写测试用例时,可直接调用封装的接口,更好的实现接口串联。

测试用例

import json
import os
import allure
import pytest
from common.get_data import GetData
from common.wrappers import write_case_log
from common.assert_cases import Assert
from utils.requests_utils import RequestUtils


def get_data():
    return GetData.get_data(os.path.basename(__file__))


class TestOffer:

    @write_case_log
    @allure.title("{data[title]}")  # 命名用例名称方式1
    @allure.severity(allure.severity_level.CRITICAL)
    @pytest.mark.parametrize("data", get_data())
    def test_create_offer(self, data, get_headers):
        applicant_id = Applicant.get_applicant_id('./data/get_applicant.yml', get_headers)
        create_offer_response = Offer.create_offer(data, get_headers,
                                                   Offer.get_offer_type('./data/get_type.yml', get_headers)['data'][0]["dataSourceResults"][0]["value"],
                                                   applicant_id,
                                                   Apply.get_apply_id('./data/get_applyid.yml', applicant_id,
                                                                      get_headers))
        # print(response.text)
        response = Offer.get_offer('./data/get_offer.yml', get_headers, create_offer_response["data"])
        # print(response)
        response = json.loads(response.text)
        # 若能根据新建的offer的offerid能查询到对应的offer,那么说明新建offer成功
        Assert.custom_assert(create_offer_response["data"], response["data"][0]["id"])

由于在前面已经对各个模块所需要的方法进行了封装,到测试用例时,直接调用封装的方法即可,已经变得十分简单且易于维护了。

在测试用例中值得关注的地方是使用的pytest的参数化,通过使用 @pytest.mark.parametrize()实现数据驱动,首先我们看一下get_data()获取到的测试数据:

[{'url': '/RecruitV6/api/v1/RecruitOnBoarding/CreateOfferInfo', 'method': 'post', 'json': '{"applyId": "a695a05d-4244-4672-b165-d2c8021eae1b","applicantId":"f36f2a64-362b-4e9b-a8b9-68b2525154d7","offerTypeID": "df5826dc-6e78-4cf4-a4fd-fc1be1f88b2e","name": "test01"}', 'code': 200, 'title': '创建Offer成功'},
 {'url': '/RecruitV6/api/v1/RecruitOnBoarding/CreateOfferInfo', 'method': 'post', 'json': '{"applyId": "a695a05d-4244-4672-b165-d2c8021eae1b","applicantId":"f36f2a64-362b-4e9b-a8b9-68b2525154d7","offerTypeID": "df5826dc-6e78-4cf4-a4fd-fc1be1f88b2e","name": "test02"}', 'code': 200, 'title': '创建Offer成功'}]

是一个列表里面嵌套了两个字典,对应的分别是两条测试用例,@pytest.mark.parametrize()会根据列表中的两个字典(一个参数多个值),执行两次测试用例。

创建offer接口的响应信息:

test_cases/test_Offer.py::TestOffer::test_create_offer[data0] 
{"data":"6917b415-f15f-4115-8080-f370e8598694","code":200,"message":null}
test_cases/test_Offer.py::TestOffer::test_create_offer[data1] 
{"data":"a33b1e8e-54b2-4515-ad27-5ac9eb036538","code":200,"message":null}

在创建offer成功后,拿到新创建的offer的offerid进行查询,若能查询到对应的offerid,则说明创建offer成功,测试用例执行通过。

test_OpenAPI/test_cases/test_Offer.py::TestOffer::test_create_offer[data0] PASSED
test_OpenAPI/test_cases/test_Offer.py::TestOffer::test_create_offer[data1] PASSED

运行完成后,使用allure生成测试报告。

测试报告展示:
基于python+requests+pytest实现接口自动化测试_第3张图片

你可能感兴趣的:(python,pytest,服务器)