python+pytest+requests+jinja2+jsonpath+jenkins接口自动化测试框架

简单介绍

  • 支持各个环境变量参数分离
  • 支持参数化,yaml测试用例文件内支持全局参数化,使用jinja2实现变量参数化,{{}} 表示变量
  • 支持前置、后置条件
  • 现支持get/post请求,上传文件
  • 支持code断言、响应模糊断言、sql断言、json提取断言
  • 支持提取上个接口响应参数给所有请求使用
  • 只需要编写yaml测试用例数据文件

目录

 

 

逻辑流程图

 

各个文件说明

assert_type.py:断言方法

# coding=utf-8

import pytest
import jsonpath
from config import Config

"""
1.assert_data: 断言数据
2.expect_value: 期望值
    code= 接口响应code
    result= 接口响应result
    message= 接口响应全部信息message
    sql= sql查询语句查询结果判断是否包含预期
    json_extract= json提取表达式提接口响应值判断是否包含预期
3.actual_value: 实际返回值
"""



def assert_type(assert_data, expect):
    # assert_data = assert_data[0].get('assert')
    if assert_data.get('code'):
        expect_value = assert_data.get('code')
        print('code 断言期望值: {}'.format(expect_value))
        # print('期望值:{}'.format(expect))
        actual_value = expect.get('code')
        print('code 返回实际值: {}'.format(actual_value))

        assert_result = pytest.assume(str(expect_value) == str(actual_value))
        print('code 断言结果: {}\n'.format(assert_result))

    if assert_data.get('result'):
        expect_value = assert_data.get('result')
        print('result 断言期望值: {}'.format(expect_value))

        actual_value = expect.get('result')
        print('result 返回实际值: {}'.format(actual_value))

        assert_result = pytest.assume(expect_value == actual_value)
        print('result 断言结果: {}\n'.format(assert_result))

    if assert_data.get('message'):
        expect_value = assert_data.get('message')
        print('message 断言期望值: {}'.format(expect_value))

        print('message 返回实际值: {}'.format(expect))

        assert_result = pytest.assume(str(expect_value) in str(expect))
        print('message 断言结果: {}\n'.format(assert_result))

    if assert_data.get('sql'):
        expect_value = assert_data.get('sql').get('expect')
        select = assert_data.get('sql').get('select')
        print('sql 断言期望值: {}'.format(expect_value))
        print('sql 断言查询语句: {}'.format(select))

        actual_value = Config().mysql(select)
        print('sql 返回实际值: {}'.format(actual_value))

        assert_result = pytest.assume(expect_value in actual_value)
        print('sql 断言结果: {}\n'.format(assert_result))

    if assert_data.get('json_extract'):
        expect_value = assert_data.get('json_extract').get('expect')
        print('json提取 断言期望值: {}'.format(expect_value))

        extract_expression = assert_data.get('json_extract').get('extract_expression')
        actual_value = jsonpath.jsonpath(expect, assert_data.get('json_extract').get('extract_expression'))
        print('json提取 断言提取表达式: {}'.format(extract_expression))
        print('json提取 断言返回实际值: {}'.format(actual_value))

        assert_result = pytest.assume(expect_value in actual_value)
        print('json提取 断言结果: {}\n'.format(assert_result))

 

function_variable.py:时间戳、随机字符串等方法

# coding=utf-8

import time
import uuid
import random


# 当前时间
def now_time():
    now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
    return now_time


# 10位时间戳
def timestamp_10():
    timestamp_10 = str(int(round(time.time())))
    print(timestamp_10)
    return timestamp_10


# 13位时间戳
def timestamp_13():
    timestamp_13 = str(int(round(time.time() * 1000)))
    print(timestamp_13)
    return timestamp_13


# 8位随机数
def str_8():
    str_8 = str(uuid.uuid4()).replace('-', '')[1:9]
    print(str_8)
    return str_8


# 16位随机数
def str_16():
    str_16 = str(uuid.uuid4()).replace('-', '')[1:17]
    print(str_16)
    return str_16


# 随机手机号
def phone_number():
    phone_number_list = ['133', '153', '180', '181', '189', '177', '173', '149',
                         '130', '131', '132', '155', '156', '176', '185', '186',
                         '135', '136', '137', '138', '150', '151', '152', '157']
    phone_number = random.choice(phone_number_list) + str(random.sample(range(0, 10), 8)).replace('[', '').replace(']', '').replace(',', '').replace(' ', '')
    print(phone_number)
    return phone_number

 

utility.py:工具方法

# coding=utf-8

import jsonpath
import yaml
from common import function_variable



# 响应提取参数组装
def response_extract(response_data, extract):
    extract_dict = {}
    for i in extract:
        extract_dict.update({i: jsonpath.jsonpath(response_data, extract.get(i))[0]})
    # print(extract_dict)
    return extract_dict


# 处理yaml文件
class DisposeYamlFile:
    def  __init__(self, path):
        self.path = path

    # 写入 yaml
    def write_yaml_file(self, data):
        with open(self.path, 'w', encoding='utf-8') as f:
            yaml.dump(data, f, allow_unicode=True)

    # 读取 yaml
    def read_yaml_file(self):
        with open(self.path, 'r', encoding='utf-8') as f:
            data = f.read()
        data = yaml.load(data, Loader=yaml.FullLoader)
        return data


# 处理文件
class DisposeFile:
    def  __init__(self, path):
        self.path = path

    # 写入 文件
    def write_file(self, data):
        with open(self.path, 'w', encoding='utf-8') as f:
            yaml.dump(data, f, allow_unicode=True)

    # 读取 文件
    def read_file(self):
        with open(self.path, 'r', encoding='utf-8') as f:
            data = f.read()
        return data

 

replace_case_param.py:用例参数处理,替换变量等

# coding=utf-8

import os
import ast
import yaml
import jinja2
from config import env
from common import function_variable



class ReadReplace:
    def __init__(self, case_file_name=''):
        path = os.path.abspath(os.path.dirname(__file__)).replace('replace_param', '')
        self.case_path = path + 'test_data/' + case_file_name + '.yaml'
        # print(self.path)

        if env() == 'test': self.variate_path = path + 'config/test/variate_data.yaml'
        if env() == 'dev': self.variate_path = path + 'config/dev/variate_data.yaml'
        # print(self.variate_path)

        if env() == 'test': self.response_path = path + 'config/test/response_data.yaml'
        if env() == 'dev': self.response_path = path + 'config/dev/response_data.yaml'
        # print(self.response_path)


    def read_response(self):
        with open(self.response_path, 'r', encoding='utf-8') as f:
            data = f.read()
        data = yaml.load(data, Loader=yaml.FullLoader)
        # print('响应提取参数:\n {}'.format(data))
        return data

    def function_variable(self):
        for  i in dir(function_variable):
            print(i)
        function_variable_value = ''

    def read_variate(self):
        with open(self.variate_path, 'r', encoding='utf-8') as f:
            data = f.read()
        data = yaml.load(data, Loader=yaml.FullLoader)
        data.update(self.read_response())
        # print('变量参数:\n {}'.format(data))
        return data

    def read_case(self):
        case_list_data = []
        with open(self.case_path, 'r', encoding='utf-8') as f:
            data = f.read()
        data = yaml.load(data, Loader=yaml.FullLoader)
        for case_data in data:
            case_list_data.append([case_data])
        # print('用例参数:\n {}'.format(case_list_data))
        return case_list_data

    def replace_data(self):
        data = ast.literal_eval(jinja2.Template(str(self.read_case())).render(self.read_variate()))
        for i in data:
            if i[0].get('type') == 'api_info':
                module = i[0].get('module')
                submodule = i[0].get('submodule')
                name = i[0].get('name')
                path = i[0].get('path')
                request_type = i[0].get('request_type')
                head = i[0].get('head')
            if not i[0].get('type'):
                i[0].update({'module': module})
                i[0].update({'submodule': submodule})
                i[0].update({'name': name})
                i[0].update({'path': path})
                i[0].update({'request_type': request_type})
                i[0].update({'head': head})
            if i[0].get('type') == 'befor' or i[0].get('type') == 'after':
                i[0].update({'module': module})
                i[0].update({'submodule': submodule})
            if i[0].get('type') == 'api_info':
                del i[0]
        data = list(filter(None, data))
        print('替换变量后用例参数:\n {}'.format(data))
        return data

 

request_api.py:请求处理

# coding=utf-8

import json
import requests
from common.utility import DisposeFile


def __request(_url, _head, _type, _param):
    if _type == 'get' and 'application/x-www-form-urlencoded' == _head.get('Content-Type'):
        try:
            r = requests.get(
                url= _url,
                headers= _head,
                data={'jsonString': json.dumps(_param)})
            return {'response_result' : r.json(), 'request_body' : r.request.body}
        except Exception as error_msg:
            print('请求错误: {}'.format(error_msg))

    if _type == 'get' and 'application/json' == _head.get('Content-Type'):
        try:
            r = requests.get(
                url= _url,
                headers= _head,
                json= _param)
            return {'response_result': r.json(), 'request_body': r.request.body}
        except Exception as error_msg:
            print('请求错误: {}'.format(error_msg))

    if _type == 'post' and 'application/x-www-form-urlencoded' == _head.get('Content-Type'):
        try:
            if 'file_path' in _param:
                r = requests.post(url = _url,
                                  headers = _head,
                                  data = json.dumps(_param),
                                  files = DisposeFile(_param.get('file')).read_file())
            else:
                r = requests.post(url = _url,
                                  headers = _head,
                                  data = {'jsonString': json.dumps(_param)})
            return {'response_result' : r.json(), 'request_body' : r.request.body}
        except Exception as error_msg:
            print('请求错误: {}'.format(error_msg))

    if _type == 'post' and 'application/json' == _head.get('Content-Type'):
        try:
            if 'file_path' in _param:
                r = requests.post(url = _url,
                                  headers = _head,
                                  json = _param,
                                  files = DisposeFile(_param.get('file')).read_file())
            else:
                r = requests.post(url=_url,
                                  headers=_head,
                                  json=_param)
            return {'response_result' : r.json(), 'request_body' : r.request.body}
        except Exception as error_msg:
            print('请求错误: {}'.format(error_msg))

  

 

config.py:配置相关

# coding=utf-8

import os
import yaml
import pymysql


def env():
    return 'test'


class Config:
    def __init__(self):
        self.path = os.path.abspath(os.path.dirname(__file__))
        if env() == 'test':
            self.config_path = self.path + '/config/test/config.yaml'
        else:
             self.config_path = self.path + '/config/dev/config.yaml'
        with open(self.config_path, 'r', encoding='utf-8') as f:
            data = f.read()
        self.data = yaml.load(data, Loader=yaml.FullLoader)

    def get_domain(self):
        domain = self.data.get('domain')
        # print('域名: {}'.format(domain))
        return domain

    def mysql(self, select):
        mysql = self.data.get('mysql')
        host = mysql.get('ip')
        port = mysql.get('port')
        user = mysql.get('user')
        pw = mysql.get('pw')
        # print('mysql: {}'.format(mysql))
        try:
            connect = pymysql.connect(host=host, port=port, user=user, password=pw, charset='utf8', connect_timeout=5)
            cur = connect.cursor()
            cur.execute(select)
            result = str(cur.fetchall()).replace('(', '').replace(')', '').replace(',', '')
            # print('sql 语句: \n{}\n'.format(select))
            # print('sql 语句执行结果: \n{}\n'.format(result))
            return result
        except Exception as error_msg:
            print('连接数据库错误: {}\n'.format(error_msg))
        finally:
            connect.close()

    def write_response(self, data):
        if env() == 'test':
            write_response_path = self.path + '/config/test/response_data.yaml'
        else:
            write_response_path = self.path + '/config/dev/response_data.yaml'
        with open(write_response_path, 'w', encoding='utf-8') as f:
            yaml.dump(data, f, allow_unicode=True)

 

conftest.py:前置处理

# coding=utf-8

import json
import pytest
from config import Config
from assert_type.assert_type import assert_type
from common.utility import response_extract
from request_api.request_api import __request

@pytest.fixture(scope='module')
def api_data(request):
    data = request.param
    for api_info in data:
        module = api_info.get('module')
        print('\n接口模块: {}'.format(module))

        submodule = api_info.get('submodule')
        print('接口子模块: {}'.format(submodule))

        name = api_info.get('name')
        print('接口名称: {}'.format(name))

        domain = Config().get_domain()
        print('域名: {}'.format(domain))

        request_type = api_info.get('request_type')
        print('请求方式: {}'.format(request_type))

        head = api_info.get('head')
        print('请求头: {}'.format(json.dumps(head)))

        path = api_info.get('path')
        print('路径: {}\n'.format(path))

        url = Config().get_domain() + str(api_info.get('path'))
        # print('接口请求url: {}'.format(url))

        case_type = api_info.get('case_type')
        print('用例类型: {}'.format(case_type))

        title = api_info.get('title')
        print('用例标题: {}'.format(title))

        param = api_info.get('param')
        # print('请求参数: {}'.format(param))

        assert_data = api_info.get('assert')
        # print('断言参数: \n{}'.format(assert_data))

        response = __request(url, head, request_type, param)
        # request_body = response.get('request_body')
        response_result = response.get('response_result')

        print('\n请求参数: \n{}\n'.format(json.dumps(param, ensure_ascii=False)))
        # print('请求参数: \n{}'.format(urllib.parse.unquote(request_body)).replace('+', ''))
        print('响应结果: \n{}\n'.format(json.dumps(response_result, ensure_ascii=False)))

        if api_info.get('assert'):
            assert_type(assert_data, response_result)
            # print('断言结果: {}'.format(assert_type(assert_data, response_result)))

        if api_info.get('response_extract'):
            response_extract_dta = response_extract(response_result, api_info.get('response_extract'))
            print('响应提取表达式: {}'.format(api_info.get('response_extract')))
            print('响应提取值: {}'.format(response_extract_dta))
            Config().write_response(response_extract_dta)

        return '{} - {}'.format(case_type, title)

 

pytest.ini:pytest运行配置

[pytest]
testpaths = test_case
python_files = test*.py *test.py
python_classes = Test* *Test
python_functions = test* *test

 

.py测试文件

# coding=utf-8


import allure
import pytest
from replace_param.replace_case_param import ReadReplace


@allure.epic('模块')
@pytest.mark.parametrize('api_data', ReadReplace('yaml用例文件').replace_data(), indirect=True)
@allure.story('子模块')
@allure.title("{api_data}")
def test(api_data):
    pass

 

.yaml测试用例数据文件

- type: "api_info" # api_info是接口信息
  module: "模块"
  submodule: "子模块"
  name: "接口名称"
  path : "接口路径"
  request_type : "请求方式"
  head :
    Content-Type : "application/x-www-form-urlencoded"
    groupCode : "BR"

- type: "befor" befor是前置条件
  case_type: '前置条件'
  name: "前置条件名称"
  title: "保存 test 供 test 使用"
  path : "路径"
  request_type: "请求方式"
  head:
    Content-Type: "application/x-www-form-urlencoded"
    groupCode: "BR"
  param : param是参数
    puser:
      userId : "{{userId}}"
      userName : "{{userName}}"
  assert : assert是断言
    code : "200"
    message : "success"

- title : "成功" # 用例标题
  case_type : "正" # 用例类型
  param :
    puser:
      userId : "{{userId}}"
      userName : "{{userName}}"
  assert :
    code : "00000"
    message : "success"
    sql :   # sql断言参数
      expect : "期望值"
      select : "sql语句"
    json_extract :   # json path提取断言参数
      expect : "期望值"
      extract_expression : "json path提取表达式"

 

html报告

posted @ 2022-01-20 20:51  Echo丶Mikasa  阅读(713)  评论(0)    收藏  举报