python 接口自动化测试框架

分享做接口测试时,搭建的测试框架思路

使用环境
windows 10
python3.7 + excel + unittest + requests + + logger + database + htmlreport

目前实现功能点:

  1. 核心通用测试框
  2. token关联
  3. 上个接口结果的值赋予下个接口的请求参数等
  4. 日志模块
  5. 数据库模块
  6. 环境配置模块
  7. 操作excel模块
  8. 测试报告模块

考虑因素:
接口数量:接口数量多,若放在脚本中维护不方便,所以使用excel来维护接口用例

一、excel元素设计

1、主用例元素
API接口常用的一些元素

元素 说明 例子
id 用例id(自定义) case_0001
title 用例标题 验证正常登录
run 是否运行标识,yes: 运行 no:不运行 yes
url 主用例接口 /Hardware/api/Test/
method 请求方法(get/post/put等) post
headers 请求头 {“Content-Type”: “application/json”}
request_data 请求参数 {“key”: “data”}
result 记录请求结果 {“ResponseStatus”:{“ErrorCode”:“0”}}
expect 预期结果 {“ErrorCode”:“0”}

2、依赖用例元素
主用例需要依赖的接口,可以做到上个接口的结果值赋予主用例接口的请求参数

元素 说明 例子
depend_url 主用例需要依赖的接口参数 {“url”:"/depend_url", “method”:“Post”,“content”:“接口描述”}
depend_request_data 依赖接口的请求参数 {“key”: “depend_data”}
depend_result 记录依赖接口的请求结果 {“ResponseStatus”:{“ErrorCode”:“0”}}
get_depend_result_for_main_case 获取依赖请求结果的某个参数赋予主用例的请求参数 {“depend_key”:“main_key”}

3、sql元素
实际项目中,有几种情况需要用到查询获取值
1.API接口中,传入的参数是数据库中的某个字段值
2.请求结果的值=数据库中的某个字段值(断言用)

元素 说明 例子
sql 查询数据库,并获取值赋予对应参数 [{“sql”:“select key from table”, “key”:“request_key”}]

预览
在这里插入图片描述

二、操作excel

上面讲了接口用例的元素设计,接下来用脚本处理
data_config:excel元素索引配置

# coding:utf-8

# 根据索引,分别对应excel中的元素,若元素有改动,在这里维护即可
class Global_Val(object):
    Id = 0
    title = 1
    run = 2
    url = 3
    request_method = 4
    header = 5
    depend_url = 6
    depend_data = 7
    depend_result = 8
    depend_key = 9
    data = 10
    sql = 11
    expect = 12
    result = 13
    is_success = 14


def get_id():
    """获取case_id"""
    return Global_Val.Id


def get_title():
    """获取用例标题"""
    return Global_Val.title


def get_url():
    """获取请求url"""
    return Global_Val.url


def get_run():
    """获取是否运行"""
    return Global_Val.run


def get_request_method():
    """获取请求方式"""
    return Global_Val.request_method


def get_header():
    """获取header"""
    return Global_Val.header


def get_depend_case():
    """case依赖"""
    return Global_Val.depend_url


def get_depend_data():
    """依赖请求数据"""
    return Global_Val.depend_data


def get_depend_result():
    """依赖请求结果"""
    return Global_Val.depend_result


def get_depend_result_key():
    """依赖请求结果参数"""
    return Global_Val.depend_key


def get_request_data():
    """请求数据"""
    return Global_Val.data


def get_sql():
    """sql数据"""
    return Global_Val.sql


def get_expect():
    """预期结果"""
    return Global_Val.expect


def get_result():
    """实际结果"""
    return Global_Val.result


def get_is_success():
    """成功/失败结果"""
    return Global_Val.is_success

get_data:获取excel数据

operation_excel:封装操作excel

import json
from data import data_config
from common.operation_excel import OperationExcel


class GetData(object):
    """获取excel数据"""

    def __init__(self, file_path, sheet_name):
        self.opera_excel = OperationExcel(file_path, sheet_name)

    def get_case_lines(self):
        """获取excel行数,即case的个数"""
        return self.opera_excel.get_max_rows()

    def get_is_run(self, row):
        """获取是否执行"""
        col = int(data_config.get_run())
        run_model = self.opera_excel.get_cell_value(row, col)
        if run_model == 'yes':
            flag = True
        else:
            flag = False
        return flag

    def is_header(self, row):
        """
        获取header
        :param row: 行号
        :return:
        """
        col = int(data_config.get_header())
        header = self.opera_excel.get_cell_value(row, col)
        if header != '':
            return json.loads(header)
        else:
            return None

    def get_request_method(self, row):
        """
        获取请求方式
        :param row: 行号
        :return:
        """
        # col 列
        col = int(data_config.get_request_method())
        request_method = self.opera_excel.get_cell_value(row, col)
        return request_method

    def get_request_url(self, row):
        """
        获取url
        :param row: 行号
        :return:
        """
        col = int(data_config.get_url())
        url = self.opera_excel.get_cell_value(row, col)
        return url

    def get_request_data(self, row):
        """
        获取请求数据
        :param row:行号
        :return:
        """
        col = int(data_config.get_request_data())
        data = self.opera_excel.get_cell_value(row, col)
        if data == '':
            return None
        # return json.loads(data)
        return eval(data)

    def get_expect_data(self, row):
        """
        获取预期结果
        :param row:
        :return:
        """
        col = int(data_config.get_expect())
        expect = self.opera_excel.get_cell_value(row, col)
        if expect == "":
            return None
        else:
            return eval(expect)

    def write_result(self, row, value):
        """
        写入结果数据
        :param value: 结果
        :param row: 行号
        :return:
        """
        col = int(data_config.get_result())
        self.opera_excel.write_value(row, col, value)

    def write_result_depend(self, row, value):
        """
        写入依赖结果数据
        :param value: 结果
        :param row: 行号
        :return:
        """
        col = int(data_config.get_depend_result())
        self.opera_excel.write_value(row, col, value)

    def write_is_success(self, row, value):
        """
        写入结果数据
        :param value: 结果
        :param row: 行号
        :param col: 列号
        :return:
        """
        col = int(data_config.get_is_success())
        self.opera_excel.write_value(row, col, value)

    def get_depend_data(self, row):
        """
        获取依赖数据的key
        :param row:行号
        :return:
        """
        col = int(data_config.get_depend_data())
        depend_data = self.opera_excel.get_cell_value(row, col)
        if depend_data == "":
            return None
        else:
            return json.loads(depend_data)

    def get_depend_key(self, row):
        """
        获取依赖请求结果的某个参数值
        :param row: 行号
        :return:
        """
        col = int(data_config.get_depend_result_key())
        depend_key = self.opera_excel.get_cell_value(row, col)
        if depend_key == "":
            return None
        else:
            return eval(depend_key)

    def is_depend(self, row):
        """
        判断是否有case依赖
        :param row:行号
        :return:
        """
        col = int(data_config.get_depend_case())  # 获取是否存在数据依赖列
        depend_url = self.opera_excel.get_cell_value(row, col)
        if depend_url == "":
            return None
        else:
            return depend_url

    def get_title(self, row):
        """
        获取用例标题
        :param row: 行号
        :return:
        """
        col = int(data_config.get_title())
        title = self.opera_excel.get_cell_value(row, col)
        if title == "":
            return None
        else:
            return title

    def get_sql(self, row):
        """
        获取sql
        :param row: 行号
        :return:
        """
        col = int(data_config.get_sql())
        sql = self.opera_excel.get_cell_value(row, col)
        if sql == "":
            return None
        else:
            # return json.loads(sql)
            return eval(sql)

三、封装HTTP请求

runmethod:使用requests库,封装HTTP请求方法

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import requests
import urllib3


class RunMethod:
    def __init__(self):
        self.session = requests.session()
        urllib3.disable_warnings()

    def run_main(self, method, url, data=None, header=None, params=None):
        """
        归类请求方法
        :param method:  请求方法
        :param url: 接口
        :param data: 请求参数
        :param header: 请求头
        :return: response(请求结果)
        """
        if method.lower() == 'Post'.lower():  # 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。
            res = self.session.post(url=url, json=data, headers=header, verify=False)
            
        elif method.lower() == 'Get'.lower():  # 请求指定的页面信息,并返回实体主体。
            res = self.session.get(url=url, json=data, headers=header, params=params, verify=False)
            
        elif method.lower() == 'HEAD'.lower():  # 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头
            res = self.session.head(url=url, json=data, headers=header, verify=False)
            
        elif method.lower() == 'PUT'.lower():  # 从客户端向服务器传送的数据取代指定的文档的内容。
            res = self.session.put(url=url, json=data, headers=header, verify=False)
            
        elif method.lower() == 'DELETE'.lower():  # 请求服务器删除指定的页面。
            res = self.session.delete(url=url, json=data, headers=header, verify=False)
            
        elif method.lower() == 'OPTIONS'.lower():  # 允许客户端查看服务器的性能。
            res = self.session.options(url=url, json=data, headers=header, verify=False)
            
        elif method.lower() == 'PATCH'.lower():  # 是对 PUT 方法的补充,用来对已知资源进行局部更新 
            res = self.session.patch(url=url, json=data, headers=header, verify=False)
        else:
            res = self.session.get(url=url, json=data, headers=header, verify=False)
        return res


if __name__ == '__main__':
    url = 'http://httpbin.org/post'
    data = {
        'cart': '11'
    }
    header = {
        "Content-Type": "text/html123123; charset=utf-8",
        "Accept-Charset": "utf-8"
    }
    run = RunMethod()
    run_test = run.run_main(method="Post", url=url, data=data, header=header)
    res_json = run_test.json()
    print(run_test.json())

requests库模块详解可以参考博客:https://www.jianshu.com/p/ada99b7880a6

四、核心逻辑

接口的核心逻辑,这里没结合unittest来做自动化,先讲一下核心逻辑:

from base.runmethod import RunMethod
from data.get_data import GetData

def base_test():
	"""
	基础测试框
	"""
	data_case = GetData(file_path, sheet_name)  # 测试用例对象
	case_count = data_case.get_case_lines  # 用例数
	
	for i in range(1, int(case_count)):
		is_run = data_case.get_is_run(i)  # 是否运行
		
        if is_run: 
            url = BASE_URL + data_case.get_request_url(i)  # 用例url
            method = data_case.get_request_method(i)  # 请求方法
            request_data = data_case.get_request_data(i)  # 请求数据
            expect = data_case.get_expect_data(i)  # 预期结果
            header = data_case.is_header(i)  # 请求头
            depend = data_case.is_depend(i)  # 依赖
            depend_data = data_case.get_depend_data(i)  # 依赖请求数据
            depend_key = data_case.get_depend_key(i)  # 依赖关键字参数
            sql_data = data_case.get_sql(i)  # 获取sql列表
			
            # 执行主用例 
            res = RunMethod().run_main(method=method, url=url, data=request_data, header=header)
    		
    		# 断言(放后面讲)
    		base_assert(i, url, header, request_data, expect, res, data_case)

到这里我们已经拿到了所有用例数据,在执行主用例之前,我们需要关注两个逻辑处理

1)查询数据库逻辑
项目中,我们经常需要用到数据库,在接口的输入参数中,有时是从数据库中存放的某个值,
同样的,在接口的输出结果参数中,有时也是存放于数据库中的某个值。
因此,我们可以设计使用sql获取值并赋予接口的输入参数或预期结果的参数

excel中sql列的内容格式:列表+字典,每个字典两个键值对,一个sql键值对,一个赋予对象键值对
例子:
[{“sql”:“select name from table + 条件”, “request”: “请求参数的某个键”}]

简单理解就是一个字典:一条sql,获取一个值,一个赋予对象(请求/依赖/预期),一个赋予对象数据的键,并且以列表为容器,支持多个字典获取值并赋予不同对象,理论上无限(sql很灵活,可以根据不同查询条件获取不同类型的值)
具体的格式,各位可以根据自己的项目实际情况来定制,这里提供的是一个思路,不一定是一成不变的

# 执行查询数据库逻辑
if sql_data is not None:
    for data in sql_data:
        sql = data["sql"]  # 获取sql
        result = DB.ExecQuery(sql)  # 使用sql第三方库,后面讲

        if result:
            result = str(result[0][0])
        else:
            raise ValueError("查询结果为空,请检查sql或数据")

        # 根据key赋值到请求数据/依赖请求数据/预期结果
        for key in data.keys():
            if key.lower() == "request":
                for_request = data[key]
                request_data[for_request] = result
            elif key.lower() == "expect":
                for_expect = data[key]
                expect[for_expect] = result
            elif key.lower() == "depend":
                for_depend = data[key]
                depend_data[for_depend] = result
            elif key.lower() == "sql":
                pass
            else:
                raise ValueError("键应为 1.request 2.expect 3.depend,请检查键是否正确")

2)依赖接口逻辑
有一些流程性的接口,上下接口有关联性,例如上个接口的输出结果中带有下一个接口输入参数需要的值,这种情况下,若只运行单个接口,而没有上个接口的输出结果参数,结果是错误的。
因此我们可以设计依赖接口逻辑,同样也是在excel中维护(excel中依赖接口的元素设计在目录一excel元素设计有讲到)

depend_url: 字典,存放url和请求方法、content为接口的描述
例子:
{
“url”:"/Hardware/TestAPI",
“method”:“Post”,
“content”:“XX接口”,
}

depend_request_data: 依赖接口的请求数据
例子:{“UserName”: “zhangsan” , “PassWord”: “123456”}

depend_result: 记录请求依赖接口的结果
例子:{“ResponseStatus”:“ErrorCode”:“0”}

get_depend_result_for_main_case: 以字典为容器,键为依赖结果的某个键,值为主用例请求数据的某个键,确定好对象,脚本中自动处理将依赖结果值赋予主用例请求数据
例子:
依赖接口的请求结果为:{“OrderID”: “DD123456”, “ResponseStatus”:“ErrorCode”:“0”}
主用例的请求数据为:{“Amount”:3, “OrderNum”: “”}
若主用例请求数据的"OrderNum"键需要依赖结果的"OrderID"的值
则填写为:{“OrderID”:“OrderNum”},即将OrderID的值赋予主用例的OrderNum键的值

# 执行依赖接口逻辑
if depend is not None:
    depend = eval(depend)  # 转字典
    depend_url = BASE_URL + depend["url"]  # 依赖接口
    depend_method = depend["method"]  # 请求方式
    depend_content = depend.get("content")  # 接口描述

    res = RunMethod().run_main(method=depend_method, url=depend_url, data=depend_data, header=header)
    data_case.write_result_depend(i, res.text)  # 写入请求结果到excel中

    # 更新依赖值到请求数据中
    if depend_key is not None:
        for key, value in depend_key.items():
            res_value = get_keyValue(res=res.json(), key=key)
            request_data[value] = res_value

同上,各位可以根据自己的需求来设计内容

3)执行顺序:数据库逻辑>依赖接口逻辑
执行完两个逻辑后,即可执行主用例和断言

# 执行主用例 
res = RunMethod().run_main(method=method, url=url, data=request_data, header=header)

# 断言(放后面讲)
base_assert(i, url, header, request_data, expect, res, data_case)

五、断言

执行完主用例后,我们需要判断请求结果是否符合我们的预期结果
设计思路:
1.实际项目中,请求的结果是存在变化性的,例如一个搜索接口,相同的搜索条件,多次请求,请求期间,业务数据有变化,多次请求的结果是不相同的,因此,不适合将整个请求结果作为预期结果来判断是否相等
2.请求的结果的关键字,实际项目中,请求的关键字一般都有会有一个code,代表这次请求结果是否正确,例如:{“code”:0,“whwswswws”:“atOSfiKraviqFf/xSxol68A==”,“openall”:1,“openalltouch”:1,“processtype”:1}
3.除了code关键字,其他关键字根据情况也可以加入预期结果进行断言

因此,我们可以设计请求结果的关键字和预期的值组成字典键值对,来进行断言,例如:{“code”:0, “processtype”:1};话不多说,上代码

def base_assert(i, url, header, request_data, expect, res, data_case):
    """
    通用断言,根据预期结果键值对>断言>实际结果,相等则成功,反之失败
    :param i: 用例行
    :param url: 用例接口
    :param header: 请求头
    :param request_data: 请求数据
    :param expect: 预期结果
    :param res: 实际结果
    :param data_case: 用例
    :return: 是否成功标识
    """
    expect_keys = list(expect.keys())
    len_keys = len(expect_keys)

    global isSuccess

    for index, key in enumerate(expect_keys):
        # 最后键标识位
        if len_keys == index + 1:
            is_last = True
        else:
            is_last = False

        # 根据预期结果的键查询响应结果对应键,并断言
        if key in res.json().keys():
            e = type(res.json()[key])(expect[key])  # 转换expect值的type为result值的type
            try:
                e == res.json()[key]
            except AssertionError as msg:
                data_case.write_result(i, res.text)  # 回写实际结果到excel
                data_case.write_is_success(i, "Fail")  # 回写成功结果到excel
                isSuccess = False
                raise AssertionError(msg)
            else:
                if is_last is True:
                    data_case.write_result(i, res.text)
                    data_case.write_is_success(i, "Success")
                    isSuccess = True
        else:
            for k, v in res.json().items():
                v_type = type(v)
                while v_type != dict:
                    if v_type is list:
                        v_list = v
                        for vv in v_list:
                            if type(vv) is dict:
                                v = vv
                                v_type = dict
                    else:
                        break
                else:
                    if key in v.keys():
                        e = type(v[key])(expect[key])  # 转换expect值的type为result值的type
                        try:
                            e == res.json()[key]
                        except AssertionError as msg:
                            data_case.write_result(i, res.text)
                            data_case.write_is_success(i, "Fail")
                            isSuccess = False
                            raise AssertionError(msg)
                        else:
                            if is_last is True:
                                data_case.write_result(i, res.text)
                                data_case.write_is_success(i, "Success")
                                isSuccess = True
    return isSuccess

六、结合unittest+ddt执行测试用例

unittest是python的单元测试框架,可以用来执行我们的测试用例和输出HTML报告

根据unittest的特性:
1.决定写一个继承unittest.TestCase的基类:base_case;
2.测试用例放在test_case文件夹中维护,这样比较清晰;

base_case:核心基础类

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import unittest
import uuid
import warnings
from base.runmethod import RunMethod
from base.base_config import BASE_URL, DB_MD
from common.check import check_response_status
from common.logger import Logger
from configs import config_manage


class BaseCase(unittest.TestCase):
	"""
	setUpClass:每次运行脚本,优先执行,只执行一次
	tearDownClass:脚本运行完成后执行,只执行一次
	"""
    @classmethod
    def setUpClass(cls) -> None:
        warnings.simplefilter('ignore', ResourceWarning)
        cls.run_method = RunMethod()
        pass

    @classmethod
    def tearDownClass(cls) -> None:
        pass

	"""
	setUp:每执行一条用例前,优先执行一次
	tearDown:每执行完一条用例后,执行一次
	"""
    def setUp(self) -> None:
        self.log = Logger(custom_color=True, name='base_case')
        self.log.info("--------------------【{}测试用例开始】--------------------".format(self.__class__.__name__ + '.' + self._testMethodName))

    def tearDown(self) -> None:
        self.log.info("--------------------【{}测试用例结束】--------------------\n".format(self.__class__.__name__ + '.' + self._testMethodName))
        self.log.del_handler()  # 防止重复写入日志

    def base_test(self, i, data_case):
        """
        核心逻辑框,所有用例都执行这里的逻辑(登录接口另外单独一个函数)
        :param i: 用例行
        :param data_case: 测试用例
        :return:
        """
        is_run = data_case.get_is_run(i)  # 是否运行
        if is_run:

            url = BASE_URL + data_case.get_request_url(i)  # 用例url
            method = data_case.get_request_method(i)  # 请求方法
            request_data = data_case.get_request_data(i)  # 请求数据
            expect = data_case.get_expect_data(i)  # 预期结果
            header = data_case.is_header(i)  # 请求头
            depend = data_case.is_depend(i)  # 依赖
            depend_data = data_case.get_depend_data(i)  # 依赖请求数据
            depend_key = data_case.get_depend_key(i)  # 依赖关键字参数
            sql_data = data_case.get_sql(i)  # 获取sql列表

            header["Authorization"] = config_manage.read_token()["Data"]["Token"]  # 更新token到请求头中

            # 执行查询数据库逻辑
            if sql_data is not None:
                for data in sql_data:
                    sql = data["sql"]  # 获取sql
                    result = DB_MD.ExecQuery(sql)

                    if result:
                        result = str(result[0][0])
                    else:
                        raise ValueError("查询结果为空,请检查sql或数据")

                    # 根据key赋值到请求数据/依赖请求数据/预期结果
                    for key in data.keys():
                        if key.lower() == "request":
                            for_request = data[key]
                            request_data = self.get_sql_value_to_request_data(request_data=request_data, key=for_request, result=result)
                        elif key.lower() == "expect":
                            for_expect = data[key]
                            expect[for_expect] = result
                        elif key.lower() == "depend":
                            for_depend = data[key]
                            depend_data = self.get_sql_value_to_request_data(request_data=depend_data, key=for_depend, result=result)
                        elif key.lower() == "sql":
                            pass
                        else:
                            raise ValueError(self.log.info("键应为 1.request 2.expect 3.depend,请检查键是否正确"))

            # 执行依赖接口逻辑
            if depend is not None:
                depend = eval(depend)  # 转字典
                depend_url = BASE_URL + depend["url"]  # 依赖接口
                depend_method = depend["method"]  # 请求方式
                depend_content = depend.get("content")  # 接口描述

                res = RunMethod().run_main(method=depend_method, url=depend_url, data=depend_data,
                                           header=header)
                data_case.write_result_depend(i, res.text)
                self.log.info("**********执行依赖:{}**********".format(depend_content))
                self.log.info("请求接口: " + depend_url)
                self.log.info("请求头:" + str(header))
                self.log.info("请求数据:" + str(depend_data))
                self.log.info("返回响应: " + res.text)
                check_response_status(res, isprint=False)  # 检查执行是否成功

                # 更新依赖值到请求数据中
                if depend_key is not None:
                    for key, value in depend_key.items():
                        res_value = self.get_keyValue(res=res.json(), key=key)
                        request_data[value] = res_value

            self.log.info("**********执行主用例**********")
            res = RunMethod().run_main(method=method, url=url, data=request_data, header=header)

            self.base_assert(i, url, header, request_data, expect, res, data_case)  # 断言

    def base_login(self, i, data_case):
        """
        登录接口用,在登陆成功后,写入token到文件中
        :param i: 用例行
        :param data_case: 测试用例对象
        :return:
        """

        is_run = data_case.get_is_run(i)
        if is_run:
            url = BASE_URL + data_case.get_request_url(i)  # 用例url
            method = data_case.get_request_method(i)  # 请求方法
            request_data = data_case.get_request_data(i)  # 请求数据
            expect = data_case.get_expect_data(i)  # 预期结果
            header = data_case.is_header(i)  # 请求头
            depend = data_case.is_depend(i)  # 依赖url相关数据
            depend_data = data_case.get_depend_data(i)  # 依赖请求数据
            depend_key = data_case.get_depend_key(i)  # 依赖关键字参数
            # sql_data = data_case.get_sql(i)  # 获取sql列表
			
			"""若需要执行数据库逻辑,或依赖接口逻辑,看base_test"""
			
            self.log.info("执行主用例".center(20, "*"))
            res = RunMethod().run_main(method=method, url=url, data=request_data, header=header)

            is_success = self.base_assert(i, url, header, request_data, expect, res, data_case)
            # 写入token到token.json文件
            if is_success is True:
                config_manage.write_token(res.json())


    def base_assert(self, i, url, header, request_data, expect, res, data_case):
        """
        通用断言,根据预期结果键值对>断言>实际结果,相等则成功,反之失败
        :param i: 用例行
        :param url: 用例接口
        :param header: 请求头
        :param request_data: 请求数据
        :param expect: 预期结果
        :param res: 实际结果
        :param data_case: 用例
        :return: 是否成功标识
        """
        expect_keys = list(expect.keys())
        len_keys = len(expect_keys)

        global isSuccess

        for index, key in enumerate(expect_keys):
            # 最后键标识位
            if len_keys == index + 1:
                is_last = True
            else:
                is_last = False

            # 根据预期结果的键查询响应结果对应键,并断言
            if key in res.json().keys():
                e = type(res.json()[key])(expect[key])  # 转换expect值的type为result值的type
                try:
                    self.assertEqual(e, res.json()[key])
                except AssertionError as msg:
                    self.log.info("请求接口: " + url)
                    self.log.info("请求头: " + str(header))
                    self.log.info("请求数据: " + str(request_data))
                    self.log.info("实际结果: " + res.text)
                    self.log.info("预期结果: " + str(expect))
                    data_case.write_result(i, res.text)
                    data_case.write_is_success(i, "Fail")
                    isSuccess = False
                    self.log.error("断言失败")
                    raise AssertionError(msg)
                else:
                    if is_last is True:
                        self.log.info("请求接口: " + url)
                        self.log.info("请求头: " + str(header))
                        self.log.info("请求数据: " + str(request_data))
                        self.log.info("预期结果:" + str(expect))
                        self.log.info("实际结果:" + res.text)
                        self.log.info("断言成功")
                        data_case.write_result(i, res.text)
                        data_case.write_is_success(i, "Success")
                        isSuccess = True
            else:
                for k, v in res.json().items():
                    v_type = type(v)
                    while v_type != dict:
                        if v_type is list:
                            v_list = v
                            for vv in v_list:
                                if type(vv) is dict:
                                    v = vv
                                    v_type = dict
                        else:
                            break
                    else:
                        if key in v.keys():
                            e = type(v[key])(expect[key])  # 转换expect值的type为result值的type
                            try:
                                self.assertEqual(e, v[key])
                            except AssertionError as msg:
                                self.log.info("请求接口: " + url)
                                self.log.info("请求头: " + str(header))
                                self.log.info("请求数据: " + str(request_data))
                                self.log.info("实际结果: " + res.text)
                                self.log.info("预期结果: " + str(expect))
                                data_case.write_result(i, res.text)
                                data_case.write_is_success(i, "Fail")
                                isSuccess = False
                                self.log.error("断言失败")
                                raise AssertionError(msg)
                            else:
                                if is_last is True:
                                    self.log.info("请求接口: " + url)
                                    self.log.info("请求头: " + str(header))
                                    self.log.info("请求数据: " + str(request_data))
                                    self.log.info("预期结果:" + str(expect))
                                    self.log.info("实际结果:" + res.text)
                                    self.log.info("断言成功")
                                    data_case.write_result(i, res.text)
                                    data_case.write_is_success(i, "Success")
                                    isSuccess = True
        return isSuccess
      

if __name__ == '__main__':
    unittest.main()

test_case:测试用例

import unittest
from base.base_case import BaseCase
from ddt import ddt, data
from data.get_data import GetData

# excel用例路径
file_path = r'E:\PyProject\Test_Pratice\test_excel.xls'


@ddt
class Test(BaseCase):
    # @unittest.skip
    """使用ddt的data方法,传递一个用例行组成的数字列表,来循环执行用例(这里可以封装一下数字列表和data_case,让代码行看起来简洁一些)"""
    @data(*[num for num in range(1, GetData(file_path, sheet_name='Login').get_case_lines())])
    def test_01_Login(self, i):
        data_case = GetData(file_path, sheet_name='Login')
        super().base_login(i, data_base)

    # @unittest.skip
    @data(*[num for num in range(1, GetData(file_path, sheet_name='AddGoods').get_case_lines())])
    def test_02_AddGoods(self, i):
        data_case = GetData(file_path, sheet_name='AddGoods')
        super().regist_and_login(i, data_base)


if __name__ == '__main__':
    unittest.main()
    pass

关联token: base_case中已经给出示例;要关联token需要2步:
1、在base_login中,执行登录接口成功后,将带有token的请求结果写入token.json文件
2、在base_test中,执行用例之前,将token赋予到请求头中

token.json:准备json文件,放在你的项目路径下
在这里插入图片描述
读写token

import json

"""TOKEN_PATH:token.json文件的路径"""

def write_token(token):
    with open(TOKEN_PATH, 'w') as fp:
        fp.write(json.dumps(token))


def read_token():
    with open(TOKEN_PATH, 'r') as fp:
        dic_data = json.load(fp)
        return dic_data

七、结合unittest+HTMLTestRunner输出报告

run_all_test_case:运行所有测试用例并输出HTML报告

import unittest
import HTMLTestRunner
from configs import config_manage
import time

if __name__ == "__main__":
	"""
	config_manage.TESTCASE_PATH:换成你测试用例存放的路径
	config_manage.REPORT_PATH:换成你测试报告想存放的路径
	"""
    suite = unittest.defaultTestLoader.discover(config_manage.TESTCASE_PATH,
                                                pattern='test_*.py',
                                                top_level_dir=None, )  # 加载路径下的所有Test开头的用例
    now = time.strftime("%Y-%m-%d %H-%M-%S")
    html_file = config_manage.REPORT_PATH + now + "_TestResult_Report.html"  # 每次生成一份新的测试报告
    # html_file = config_manage.REPORT_PATH + "TestResult_Report.html"  # 固定生成一份测试报告
    '''wb以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。'''
	fp = open(html_file, "wb")
    runner = HTMLTestRunner.HTMLTestRunner(stream=fp, title='接口自动化测试报告', description='用例执行情况:', )
    runner.run(suite)
    fp.close()

HTMLTestRunner库:放在python版本的Lib目录下即可,例如:E:\Python37\Lib
链接:https://pan.baidu.com/s/1xTRX1X5S-zagkILEDVIMtQ
提取码:c9a1

效果:
在这里插入图片描述
分享下BeautifulReport库,我觉得这个更好看点

import unittest
from BeautifulReport import BeautifulReport
from configs import config_manage
import time

if __name__ == "__main__":
	"""
	config_manage.TESTCASE_PATH:换成你测试用例存放的路径
	config_manage.REPORT_PATH:换成你测试报告想存放的路径
	"""
    suite = unittest.defaultTestLoader.discover(config_manage.TESTCASE_PATH,
                                                pattern='test_*.py',
                                                top_level_dir=None, )  # 加载路径下的所有Test开头的用例
    now = time.strftime("%Y-%m-%d %H-%M-%S")
    html_file = config_manage.REPORT_PATH + now + "_TestResult_Report.html"  # 每次生成一份新的测试报告
    # html_file = config_manage.REPORT_PATH + "TestResult_Report.html"  # 固定生成一份测试报告

    runner = unittest.TextTestRunner()
    BeautifulReport(suite).report(filename=now + "_TestResult_Report.html", description='接口测试', log_path=config_manage.REPORT_PATH)
    runner.run(suite)

效果:
在这里插入图片描述
在这里插入图片描述

八、环境配置

配置是项目中必不可少的一部分,我们经常会用到各种不同的配置,例如:IP,账号,密码,数据库配置,项目路径等,若分散在不同文件中,散乱且难维护,所以我们可以设计一个存放配置的方法

configs.yaml:yaml文件存放配置参数(yaml模块安装和使用讲解:yaml模块

# 账号配置
URL: http://localhost:9998
数据:
  用户名: chen
  密码: 123456

# 数据库配置
sqlServer:
  host: .
  port: 1433
  user: sa
  pwd:  123456
  database: Tester

config_manage:管理环境配置参数

# coding:utf-8
import yaml
import os
import json


def get_yaml_config():
    f = open(YAML_PATH, 'r', encoding='UTF-8').read()  # 只读模式打开并读取字符串
    dic = yaml.load(f, Loader=yaml.FullLoader)  # 将配置参数转为字典格式
    return dic


if __name__ == "__main__":
    get_config = get_yaml_config()
    print(get_config['URL'])

我们经常需要写入文件,或者读取目录/文件,例如:读取/写入excel、日志、报告等存放的路径,所以我们也可以设置一些项目文件目录的常量,统一管理,在操作的时候,直接使用定义好的常量即可;
用的是os库,想了解可以看:os模块

# 项目相关文件目录常量
PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))  # 项目路径
TESTDATA_PATH = os.path.join(PROJECT_PATH, "test_data")  # 测试数据目录
TESTCASE_PATH = os.path.join(PROJECT_PATH, "test_case" + os.sep)  # 测试用例目录
LOG_PATH = os.path.join(PROJECT_PATH, "test_report" + os.sep + "logs" + os.sep)  # 日志目录
REPORT_PATH = os.path.join(PROJECT_PATH, "test_report" + os.sep + "report" + os.sep)  # 测试报告目录
CONFIG_PATH = os.path.join(PROJECT_PATH, "configs" + os.sep)  # 配置文件目录
TOKEN_PATH = os.path.join(CONFIG_PATH, "token.json")  # token文件

完整代码:

# coding:utf-8
import yaml
import os
import json


# 项目相关文件目录常量
PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))  # 项目路径
TESTDATA_PATH = os.path.join(PROJECT_PATH, "test_data")  # 测试数据目录
TESTCASE_PATH = os.path.join(PROJECT_PATH, "test_case" + os.sep)  # 测试用例目录
LOG_PATH = os.path.join(PROJECT_PATH, "test_report" + os.sep + "logs" + os.sep)  # 日志目录
REPORT_PATH = os.path.join(PROJECT_PATH, "test_report" + os.sep + "report" + os.sep)  # 测试报告目录
CONFIG_PATH = os.path.join(PROJECT_PATH, "configs" + os.sep)  # 配置文件目录
TOKEN_PATH = os.path.join(CONFIG_PATH, "token.json")  # token文件


def get_yaml_config():
    f = open(YAML_PATH, 'r', encoding='UTF-8').read()  # 只读模式打开并读取字符串
    dic = yaml.load(f, Loader=yaml.FullLoader)  # 将配置参数转为字典格式
    return dic


def write_token(token):
    with open(TOKEN_PATH, 'w') as fp:
        fp.write(json.dumps(token))


def read_token():
    with open(TOKEN_PATH, 'r') as fp:
        dic_data = json.load(fp)
        return dic_data


if __name__ == "__main__":
    get_config = get_yaml_config()
    print(get_config['URL'])
    data = {"token": "test"}
    write_token(token=data)
    print(read_token())

后部分有时间更新

九、日志模块

十、数据库操作模块

十一、发送邮件模块

PS:有缘的人儿,有收获的话,留个爪点个赞呗

原创文章 5 获赞 13 访问量 498

猜你喜欢

转载自blog.csdn.net/weixin_47154909/article/details/105984243