Python测试开发-重写unittest

小记:很久没写博客了,一直在忙着写一些工具/平台之类的效能。最近在写自己的接口自动化的执行驱动。需要重写下uniittest,就研究了下。

思路

  • 首先需要整理uniittest的模块(类)以及明白各个类之间的作用
  • 再次需要明白哪些是需要重写的,哪些是不需要重写的
  • 对需要重写的类进行熟悉
  • 明白所实现的需求,有针对性的的去重写类

具体实现

unittest的整理

unittest主要用到的类以及类的作用
image

想一想unittest中哪些是需要我们重写的?

首先,针对TestCase的作用来说,是不需要我们重写的
其次,TestLoader的作用是收集用例的,也是不需要重写的
再次,TestSuit的作用是存放套件的,也是不需要重写的,用原本的就好
再次,TestRunner的作用是执行测试用例的,因为执行不知道是按照什么套件粒度来执行,或者你需要多线程执行,那么这个类我们必然是需要重写的
最后,TestResult的租用是收集测试结果的,必然是需要重写的,因为我们需要收集测试用例的结果或者日志或者详细的错误或者其他

针对TestRunner和TestResult类进行熟悉

TestRunner源码

image

可以看出这个类中只有一个run方法,那么我们看参数是一个TestSuit类型,这个时候我们就明白了那其实就是执行了run方法并传递了测试套件给了run

TestResult源码

image
image
截图标记的方法可能就是需要我们重写的方法了

明白我们需要实现的需求

通过测试数据来生成测试类(这里会讲测试执行器给编写出来)

点击查看代码
import unittest
from concurrent.futures.thread import ThreadPoolExecutor

from core.custom_case_result import TestResult
from project_common.base_case import MyMateClass, TestBaseCase
from project_common.handle_data import HandleData


def custom_class(data: list):
    """
    自定义测试类
    :param data: 测试数据,属于一个列表
    :return: 返回个测试类列表
    """
    test_obj_class = []
    for case_data in data:
        obj = MyMateClass(f'Test{case_data["name"]}', (TestBaseCase,),
                          {"data": case_data["test_suit"]})
        test_obj_class.append(obj)
    return test_obj_class


def run(data, max_th):
    """
    测试用例执行器
    :param data: 测试数据
    :param max_th: 几个线程执行测试用例
    :return: 
    """
    suit_list = []
    for i in custom_class(data):
        # 将测试类作为一个suit
        suit = unittest.defaultTestLoader.loadTestsFromTestCase(i)
        suit_list.append(suit)
    result = TestResult()
    result.startTestRun()
    with ThreadPoolExecutor(max_workers=max_th) as th:
        for i in suit_list:
            th.submit(i.run, result)
    result.stopTestRun()

通过继承TestCase来编写我们的测试流程

可能有人会问,之前不是说不重写这玩意吗?你这不是打脸?
解释:因为这里的继承写,其实不是重写这个类,而是需要将我们的测试类编写一个流程下来,进行完善我们的测试流程

点击查看代码
import json
import re
import unittest
from functools import wraps
from loguru import logger

import jsonpath

import public_method_pck
from project_common.request_handle import send_request
from project_common.handle_db import HandleDataBase


# 修改测试方法,将测试数据传递给测试方法
def decorator(func, data):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        return func(self, data, *args, **kwargs)

    wrapper.__doc__ = data.get("desc")
    return wrapper


def case_name_decorator(index, case_name):
    if index + 1 < 10:
        test_name = case_name + "_00" + str(index + 1)
    elif index + 1 < 100:
        test_name = case_name + "_0" + str(index + 1)
    else:
        test_name = case_name + "_" + str(index + 1)
    return test_name


class MyMateClass(type):
    """自定义的元类,用来生成测试类的"""

    def __new__(cls, name, bases, attr, *args, **kwargs):
        my_cls = super().__new__(cls, name, bases, attr)
        # 处理参数值attr,使用参数动态生成测试方法

        for index, values in enumerate(attr["data"]):
            new_func = decorator(my_cls.case_func, values)  # 利用装饰器修改测试方法,使其可以传递参数
            case_func = case_name_decorator(index, f"test_{values.get('title')}")
            # case_func = f"test_{values.get('title')}"  # 动态根据参数生成用例方法名
            # func2 = cls.__update_func(case_func, values)
            setattr(my_cls, case_func, new_func)  # 设置动态生成的测试类中的测试方法
        return my_cls


class TestBaseCase(unittest.TestCase):
    """
    用例基类
    """
    name = None

    # def __str__(self):
    #     return self._case["title"]

    @classmethod
    def setUpClass(cls) -> None:
        logger.info(f"=========【{cls.__name__[4:]}】接口开始测试===========")

    @classmethod
    def tearDownClass(cls) -> None:
        logger.info(f"==========【{cls.__name__[4:]}】接口结束测试===========")

    def case_func(self, item):
        """
        接口测试执行流程
        :param item:
        :return:
        """
        self.log_list = []
        self.log_list.append('用例【{}】开始执行\n'.format(item['title']))
        logger.info('>>>>>>>用例【{}】开始执行>>>>>>>>'.format(item['title']))
        # 将测试数据绑定到_case属性上,方便操作
        self._case = item
        try:
            """数据处理"""
            self.process_test_data()
            """发送请求"""
            self.send_request()
            """断言"""
            self.assert_all()
            self.log_list.append(f"用例【{self._case['title']}】测试成功\n")
            logger.info('用例【{}】测试成功<<<<<<<<<'.format(self._case['title']))
        except Exception as e:
            self.log_list.append('用例【{}】测试失败\n'.format(self._case['title']))
            logger.error('用例【{}】测试失败<<<<<<<<<'.format(self._case['title']))
            raise e
        finally:
            logger.info('<<<<<<<<<用例【{}】测试结束<<<<<<<'.format(self._case['title']))

    def process_test_data(self, cls=public_method_pck):
        """测试数据处理
        1、处理测试数据中的动态数据
        2、处理测试数据中的变量数据


        """
        data = json.dumps(self._case["request"])
        temp_data = re.findall(r'\^(.*?)\^', data)
        for arg in temp_data:
            logger.info("需要替换的动态数据为:{}".format(arg))
            value = getattr(cls, arg, None)
            if value:
                data = data.replace(f'^{arg}^', str(value()))
                logger.info(f"{arg}数据替换成功,新值为:{str(value())}")
            else:
                logger.error(f"{arg}数据替换失败,因为在{cls}中不存在{value}对象")
        # _new_data = json.loads(data)
        # logger.info("=========获取到的原始测试数据:{}===========".format(json.loads(data)["json"]))
        # logger.info(f"替换完成的数据为:{_new_data}")
        # 处理测试数据是否存在类变量,如果存在则去类变量中找到并替换
        # case = json.dumps(self._case)
        _data = re.findall(r"==(.*?)==", data)
        if _data:
            for item in _data:
                logger.info(f"需要替换的类变量数据为:{item}")
                _value = getattr(self.__class__, item, None)
                if _value:
                    case_data = data.replace(f"=={item}==", _value)
                    logger.info(f"{item}数据替换成功,新值为:{_value}")
                else:
                    logger.error(f"{item}数据替换失败,因为在{self.__class__}中不存在{item}对象")
            new_data = json.loads(case_data)
        else:
            new_data = json.loads(data)
        logger.info(f"替换完成的数据为:{new_data}")
        self._case["request"] = new_data
        return self._case

    def send_request(self):
        """发送请求"""
        try:
            logger.info("开始发起接口请求")

            self._response = send_request(url=self._case['url'],
                                          method=self._case['method'],
                                          **self._case["request"]
                                          )
            logger.info("发起接口请求成功了")
            logger.info(f"请求body数据为:{self._case['request']['json']}")
            logger.info(f"请求headers数据为:{self._case['request']['headers']}")
        except Exception as e:
            logger.warning('用例【{}】发送http请求错误:{}'.format(self._case['title'], e))
            logger.warning('url: 【{}】'.format(self._case['url']))
            logger.warning("method: 【{}】".format(self._case['method']))
            logger.warning('args: {}'.format(self._case['request']))
            raise RuntimeError("用例【{}】发送HTTP请求错误: {}".format(self._case['title'], e))

    def assert_res_all(self):
        """
        响应断言
        :return:
        """
        if self._case["assertion"]:
            # 拿到断言列表进行循环
            for i in self._case["assertion"]:

                try:
                    assert_data = jsonpath.jsonpath(self._response.json(), i[1])[0]
                    logger.info(f"开始断言...\n需要断言的方式为:{i[0]}, 预期结果为:{i[-1]}, 实际结果为:{assert_data}")

                    if i[0] == "eq":
                        self.assertEqual(assert_data, i[-1])

                    elif i[0] == "in":
                        self.assertIn(i[-1], assert_data)
                    else:
                        logger.warning("==========该断言方式暂时不支持========")
                except AssertionError as e:
                    logger.warning("用例【{}】断言异常".format(self._case["title"]))
                    logger.warning("请求参数:{}".format(self._case['request']))
                    logger.warning("响应:{}".format(self._response.json()))
                    raise e
                else:
                    logger.info('用例【{}】响应状态码断言成功'.format(self._case['title']))

        else:
            logger.info("用例【{}】不需要断言".format(self._case["title"]))

    def assert_db_all(self):
        """数据库断言, 目前只支持相等判断"""
        # TODO:实现数据库非相等断言
        # 判断测试用例中是否存在database_assertion,如果存在则进行数据库断言

        database_assertion = self._case.get("database_assertion")
        if database_assertion:
            for i in database_assertion:
                database_name = i[0]
                actual_results = i[1]
                sql = i[2]
                num = i[-1]
                try:
                    db = HandleDataBase(db=database_name)
                    try:
                        res = db.get_db_data(sql, num)
                    except Exception as e:
                        logger.error(f"sql语句为:{sql}, 执行SQL语句有问题")
                        raise e
                    self.assertEqual(actual_results, res)
                    logger.info('用例【{}】数据库状态码断言成功'.format(self._case['title']))
                except AssertionError as e:
                    logger.warning(f"【{self._case['title']}】断言失败,实际结果为:{res}, 期待结果为:{actual_results}")
                    raise e

    def assert_all(self):
        """断言集中"""
        self.assert_res_all()
        self.assert_db_all()
        self.extract_data()

    def extract_data(self):
        """
        提取依赖数据
        :return:
        """
        rules = self._case.get("extract")
        if rules:
            logger.info("开始执行后置,需要提取依赖数据...")
            for rule in rules:
                # 类属性名
                name = rule[1]
                # 提取表达式
                exp = rule[-1]
                if rule[0] == "req":
                    obj = self._case["request"]
                elif rule[0] == "res":
                    obj = self._response.json()
                else:
                    raise ValueError("提取依赖数据的方式错误,只支持req和res")
                value_data = jsonpath.jsonpath(obj, exp)
                if value_data:
                    setattr(self.__class__, name, value_data[0])
                    logger.info("提取的依赖数据为:{}={}".format(name, value_data[0]))
                else:
                    raise ValueError('用例【{}】的提取表达式【{}】提取不到数据'.format(self._case['title'], self._case['extract']))
        else:
            logger.info("用例【{}】不需要提取依赖数据".format(self._case["title"]))


重写测试结果类(重写了TestResult)

点击查看代码
import time
import unittest


class TestResult(unittest.TestResult):
    """重写TestResult类,自定义收集测试结果"""
    result = {
        'all': 0,
        "success": 0,
        'fail': 0,
        'error': 0,
        'skip': 0,
        'cases': [],
        'run_time': "",
        'testClass': set()
    }

    def startTest(self, test):
        """
        当用例执行前进行调用
        :param test:
        :return:
        """
        super(TestResult, self).startTest(test)
        self.start_time = time.time()  # 获取用例执行的开始时间

    def stopTest(self, test: unittest.case.TestCase) -> None:
        """
        测试用例执行完后进行调用
        :param test:
        :return:
        """
        # 获取用例执行时间
        test.run_time = "{:.3}s".format(time.time() - self.start_time)
        # 获取测试用例测试类名
        test.class_name = test.__class__.__qualname__
        # 获取测试用例方法名
        test.method_name = getattr(test, "_testMethodName", "获取用例名称失败")
        # 获取测试用例的描述
        test.method_desc = test.shortDescription()
        info = {
            "测试用例名称": test.method_name,
            "用例执行状态": getattr(test, "status"),
            "用例error": getattr(test, "errorMsg", ""),
            # "用例数据": test._case.get('request')
            "日志": "".join(test.log_list)
        }
        self.__class__.result["cases"].append(info)

    def addSuccess(self, test: unittest.case.TestCase) -> None:
        """
        当用例成功的时候会调用
        :param test:
        :return:
        """
        self.__class__.result["success"] += 1
        super(TestResult, self).addSuccess(test)
        test.status = "成功"

    def addError(self, test: unittest.case.TestCase, err) -> None:
        """当测试用例运行错误的时候会调用"""
        super().addError(test, err)
        # print("error:", err)
        # print(test._response.json())
        self.__class__.result["error"] += 1
        test.errorMsg = err
        test.status = "错误"

    def addFailure(self, test: unittest.case.TestCase, err) -> None:
        """当测试用例运行失败的时候会被调用"""
        super().addFailure(test, err)
        # print("fail", err)
        # print(test._response.json())
        self.__class__.result["fail"] += 1
        test.errorMsg = err
        test.status = "失败"

    def addSkip(self, test: unittest.case.TestCase, reason: str) -> None:
        """当测试用例被跳过的时候调用"""
        super().addSkip(test, reason)
        # print("skip", reason)
        self.__class__.result["skip"] += 1

    def startTestRun(self) -> None:
        super().startTestRun()
        self.s_time = time.time()

    def stopTestRun(self) -> None:
        """当所有的测试用例运行完成后会被调用"""
        super().stopTestRun()
        self.__class__.result["all"] = sum((self.__class__.result["success"],
                                            self.__class__.result["fail"],
                                            self.__class__.result["error"],
                                            self.__class__.result["skip"]))
        self.__class__.result["run_time"] = time.time() - self.s_time
        print(self.__class__.result)
测试结果类实现的需求: - 收集了所有的测试用例数量以及成功数、失败数、错误数等 - 收集了每条测试用例执行的详细数据、执行日志等 - 收集了用例的执行时间

到此也差不多结束了,这个也是一个demo,一个思路而已,有很多不足之处还需要改进

posted @ 2022-12-15 12:49  大渝  阅读(104)  评论(0编辑  收藏  举报