单元测试简单示例:python+unittest+ddt+HTMLTestRunner+config配置文件(重在思路)

2023-11-13

这是一个基于数学运算加减法的单元测试示例。重点在于单元测试的设计思路梳理。
目的:测试数据从excel中获取,执行后并将结果写入excel,并生成报告。
重点:可根据代码中的注释进行帮助理解设计思路。

下面是我的测试结构。
在这里插入图片描述
下面来解释一下各个目录及对应文件的作用:
【Unit_Test】项目根目录。
【Calculation_Method】是存放测试代码的目录。
【Calculation.py】是被测试的代码类,里面就是我的数算运算方法。
【Cases_Excels】是存放用例excel的目录。
【cases.xlsx】是用例中具体的数据。
【Conf】是存放配置文件及处理配置文件方法的目录。
【conf.conf】是配置文件,主要是存放不长变更的常量。
【handle_config.py】封装的处理解析配置文件的类。
【HandleOption】存放处理类的方法的目录。目前仅有处理excel的方法。
【handle_excel.py】封装的处理excel的类。
【log】存放log相关的目录。生成的日志按照日期+cases.log命名
【handle_logging.py】封装的收集日志的类。
【modification_ddt】存放修改后的ddt文件。
【ddt.py】从site-packages里的ddt复制出来的,主要修改了用例命名。
【Reports】是存放执行用例报告的目录。为了避免覆盖,使用日期+report.html命名。
【Run_Cases】是执行用例的驱动的目录。
【discover_cases.py】通过discover来发现并批量执行用例。类文件需要以test_开头
【moudle_cases.py】通过模块导入并加载用例。执行某些模块的用例。
【Unit_Test_Cases】存放测试用例的目录。
【test_addition_cases.py】加法的测试用例的类。
【test_subtraction_cases.py】减法的测试用例的类。

附上各个文件的代码:
【Calculation.py】

class Calculation:
    def __init__(self, num1, num2):
        self.num1 = num1
        self.num2 = num2

    def addition(self):  # 加法
        return self.num1 + self.num2

    def subtraction(self):  # 减法
        return self.num1 - self.num2

    def multiplication(self):  # 乘法
        self.num1 * self.num2

    def division(self):  # 除法
        if self.num2 == 0:
            raise Exception("除数为0")
        return self.num1 / self.num2

【conf.conf】


# 配置文件,配置不经常变化的常量,如果有变化,更改此处的值,但不要修改区域名称和key值
# 区域名称  key及对应值

# path 主要存放各个路径文件,绝对路径或者相对路径
[path]
# 测试用例使用的数据的excel存放路径
cases_excel_path = D:\\PythonWorkFolder\\Unit_Test\\Cases_Excels\\cases.xlsx
# 日志存放路径
log_path = D:\\PythonWorkFolder\\Unit_Test\\log\\logfile.txt
# 报告存放路径
report_path = D:\\PythonWorkFolder\\Unit_Test\\Reports\\


# 不同用例对应的页签名称
[name]
# 加法运算的页签名称
add_sheet_name = add
# 减法运算的页签名称
subtraction_sheet_name = subtraction

# 主要存放excel的相关信息
[excel]
# excel中存放实际结果的列数
actual_column = 6
# excel 中存放测试结果的列
result_column = 7
# 执行通过
pass_result = Pass
# 执行不通过
fail_result = Fail

[log]
# 日志收集器名称
logger_name = case_log
# 日志收集器的级别
logger_level = DEBUG
# 存放日志的地址
log_path = D:\\PythonWorkFolder\\Unit_Test\\log\\
# 输出的日志名称
log_filename = cases.log
# 输出到控制台的日志级别
console_level = ERROR
# 输出到日志文 件中的级别
file_level = DEBUG
# 日志输出内容, 两个%代表转义
simple_formatter = %%(asctime)s - [%%(levelname)s] - [msg]: %%(message)s
verbose_formatter = %%(asctime)s - [%%(levelname)s] - %%(lineno)d - %%(name)s - [msg]: %%(message)s

【handle_config.py】

"""
封装配置处理的类 
"""


# 通过[] 或者get方法,读取到的所有的值都是字符串类型
# 通过getint方法获取到的是int类型
# 通过getfloat方法获取到float类型
# 通过getboolean方法获取到布尔类型:配置中的值1 、yes 、 on、 true 、 True 都会读取成True; 0、 no、 off、 false、 False都会读取成False
# eval函数可以将字符串转化为python中内置的数据类型, 也能够执行字符串类表达式。相当于将字符串类型的引号拿掉之后的类型

from configparser import ConfigParser


class HandleConfig:
    def __init__(self, filename):
        self.filename = filename
        self.config = ConfigParser()
        self.config.read(self.filename, encoding="utf-8")

    def get_value(self, section, option):
        return self.config.get(section, option)  # 字符串

    def get_int(self, section, option):  # int类型
        return self.config.getint(section, option)

    def get_float(self, section, option):  # float类型
        return self.get_float(section, option)

    def get_boolean(self, section, option):
        return self.get_boolean(section, option)

    def get_eval_data(self, section, option):
        return eval(self.config.get(section, option))

    @staticmethod  # 与上面对象没有关系,所以定义为静态方法
    def write_config(datas, filename):
        if isinstance(datas, dict):  # 判断外层是否是字典
            for value in datas.values():
                if not isinstance(value, dict):  # 判断值是不是字典
                    return "数据不合法"
            config = ConfigParser()
            for key in datas:
                config[key] = datas[key]
            with open(filename, 'w') as file:
                config.write(file)


# 创建一个对象
do_config = HandleConfig(r"D:\PythonWorkFolder\Unit_Test\Conf\conf.conf")

【handle_excel.py】封装的处理excel的类。

from openpyxl import load_workbook
from Conf.handle_config import do_config


class HandleExcel:
    def __init__(self, file_name, sheet_name=None):
        """
        需要两个参数, 文件名称和页签名称, 页签名称默认为空
        """
        self.filename = file_name
        self.sheet_name = sheet_name

    """
    封装好的方法,一般不要再进行改动,好的方法,具有很强大的扩展性
    """
    def get_cases(self):
        """
        获取所有的用例
        """
        wb = load_workbook(self.filename)  # 获取excel对象
        if self.sheet_name is None:
            ws = wb.active  # 如果页签名称未传入,则默认取第一个页签, active默认取第一个页签
        else:
            ws = wb[self.sheet_name]
        # 取除excel第一行的值,并转化为一个元组后,取出第一个元组,就是数据用例中的的表头
        head_data_tuple = tuple(ws.iter_rows(max_row=1, values_only=True))[0]
        cases_list = []  # 定义一个空列表,用来存储读取出来的用例
        for one_tuple in tuple(ws.iter_rows(min_row=2, values_only=True)):
            # 将循环取出的每一列值和对应的读取的表头转化为一组字典, 然后将每一组字典写入到列表中,得到一个嵌套字典的列表
            cases_list.append(dict(zip(head_data_tuple, one_tuple)))
        return cases_list

    def get_one_case(self, row):
        """
        需要指定行号
        """
        cases = self.get_cases()
        return cases[row-1]  # 列表的索引是从0开始的,所以需要减去1

    def write_result(self, row, actual, result):
        """
        传入行号,实际结果和测试结果
        写入和读取最好不要使用同一个对象,所以这里需要再定义一个新的对象, 如果是同一个对象,只有最后一个会写入成功,这是openpyxl的特性
        """
        print(row)
        actual_column = do_config.get_int("excel", "actual_column")
        result_column = do_config.get_int("excel", "result_column")
        wb1 = load_workbook(self.filename)
        if self.sheet_name is None:
            ws1 = wb1.active
        else:
            ws1 = wb1[self.sheet_name]
        if isinstance(row, int) and (2 <= row <= ws1.max_row):  # 判断,行数是不是在2和最大行数之间
            ws1.cell(row, column=actual_column, value=actual)
            ws1.cell(row, column=result_column, value=result)
            wb1.save(self.filename)
            wb1.close()
        else:
            print("输入的行号不正确")


if __name__ == "__main__":
    filename = r'D:\PythonWorkFolder\Unit_Test\Cases_Excels\cases.xlsx'
    w = HandleExcel(filename)
    w.get_cases()

【handle_logging.py】封装的收集日志的类。

import logging
from datetime import datetime
from Conf.handle_config import do_config


class HandleLog:
    """
    封装日志处理的类
    """
    def __init__(self):
        # 获取日志收集名称
        logger_name = do_config.get_value("log", "logger_name")
        # 定义日志收起器, 创建logger对象
        self.case_logger = logging.getLogger(logger_name)

        # 日志等级 NOTSET(0), DEBUG(10), INFO(20), WARNING(30), ERROR(40), CRITICAL(50)
        # 设置之后,只能收集当前等级及以上的日志信息。如设置为warning级别,只能手机warning、error、critical等级的
        # case_logger.setLevel(logging.DEBUG)
        self.case_logger.setLevel(do_config.get_value("log", "logger_level"))

        # 定义日志输出渠道
        # 输出到控制台
        console_handler = logging.StreamHandler(do_config.get_value("log", "console_level"))
        # 输出到文件
        log_name = do_config.get_value("log", "log_path") + datetime.strftime(datetime.now(), '%Y_%m_%d_%H_%M_%S') + do_config.get_value("log", "log_filename")
        file_handler = logging.FileHandler(log_name,
                                           encoding='utf-8')

        # 日志输出渠道的等级, 日志输出的等级,不能高于收集器的等级
        # console_handler.setLevel(logging.ERROR) 与 console_handler.setLevel("DEBUG")等价
        console_handler.setLevel(do_config.get_value("log", "console_level"))
        file_handler.setLevel(do_config.get_value("log", "file_level"))

        # 定义日志显示的格式
        # 简单格式
        simple_formatter = logging.Formatter(do_config.get_value("log", "simple_formatter"))
        # 稍微详细的格式
        verbose_formatter = logging.Formatter(do_config.get_value("log", "verbose_formatter"))

        # 控制台显示简单日志
        console_handler.setFormatter(simple_formatter)
        # 文件显示稍微详细的日志
        file_handler.setFormatter(verbose_formatter)

        # 将日志收集器与输出渠道对接
        self.case_logger.addHandler(console_handler)
        self.case_logger.addHandler(file_handler)

    def get_logger(self):
        return self.case_logger


do_log = HandleLog().get_logger()


if __name__ == "__main__":
    do_log.debug("debug")

【ddt.py】

# -*- coding: utf-8 -*-
# This file is a part of DDT (https://github.com/datadriventests/ddt)
# Copyright 2012-2015 Carles Barrobés and DDT contributors
# For the exact contribution history, see the git revision log.
# DDT is licensed under the MIT License, included in
# https://github.com/datadriventests/ddt/blob/master/LICENSE.md

import codecs
import inspect
import json
import os
import re
from enum import Enum, unique
from functools import wraps

try:
    import yaml
except ImportError:  # pragma: no cover
    _have_yaml = False
else:
    _have_yaml = True

__version__ = '1.4.2'

# These attributes will not conflict with any real python attribute
# They are added to the decorated test method and processed later
# by the `ddt` class decorator.

DATA_ATTR = '%values'  # store the data the test must run with
FILE_ATTR = '%file_path'  # store the path to JSON file
YAML_LOADER_ATTR = '%yaml_loader'  # store custom yaml loader for serialization
UNPACK_ATTR = '%unpack'  # remember that we have to unpack values
index_len = 5  # default max length of case index

try:
    trivial_types = (type(None), bool, int, float, basestring)
except NameError:
    trivial_types = (type(None), bool, int, float, str)


@unique
class TestNameFormat(Enum):
    """
    An enum to configure how ``mk_test_name()`` to compose a test name.  Given
    the following example:

    .. code-block:: python

        @data("a", "b")
        def testSomething(self, value):
            ...

    if using just ``@ddt`` or together with ``DEFAULT``:

    * ``testSomething_1_a``
    * ``testSomething_2_b``

    if using ``INDEX_ONLY``:

    * ``testSomething_1``
    * ``testSomething_2``

    """
    DEFAULT = 0
    INDEX_ONLY = 1


def is_trivial(value):
    if isinstance(value, trivial_types):
        return True
    elif isinstance(value, (list, tuple)):
        return all(map(is_trivial, value))
    return False


def unpack(func):
    """
    Method decorator to add unpack feature.

    """
    setattr(func, UNPACK_ATTR, True)
    return func


def data(*values):
    """
    Method decorator to add to your test methods.

    Should be added to methods of instances of ``unittest.TestCase``.

    """
    global index_len
    index_len = len(str(len(values)))
    return idata(values)


def idata(iterable):
    """
    Method decorator to add to your test methods.

    Should be added to methods of instances of ``unittest.TestCase``.

    """

    def wrapper(func):
        setattr(func, DATA_ATTR, iterable)
        return func

    return wrapper


def file_data(value, yaml_loader=None):
    """
    Method decorator to add to your test methods.

    Should be added to methods of instances of ``unittest.TestCase``.

    ``value`` should be a path relative to the directory of the file
    containing the decorated ``unittest.TestCase``. The file
    should contain JSON encoded data, that can either be a list or a
    dict.

    In case of a list, each value in the list will correspond to one
    test case, and the value will be concatenated to the test method
    name.

    In case of a dict, keys will be used as suffixes to the name of the
    test case, and values will be fed as test data.

    ``yaml_loader`` can be used to customize yaml deserialization.
    The default is ``None``, which results in using the ``yaml.safe_load``
    method.
    """

    def wrapper(func):
        setattr(func, FILE_ATTR, value)
        if yaml_loader:
            setattr(func, YAML_LOADER_ATTR, yaml_loader)
        return func

    return wrapper


def mk_test_name(name, value, index=0, name_fmt=TestNameFormat.DEFAULT):
    """
    Generate a new name for a test case.

    It will take the original test name and append an ordinal index and a
    string representation of the value, and convert the result into a valid
    python identifier by replacing extraneous characters with ``_``.

    We avoid doing str(value) if dealing with non-trivial values.
    The problem is possible different names with different runs, e.g.
    different order of dictionary keys (see PYTHONHASHSEED) or dealing
    with mock objects.
    Trivial scalar values are passed as is.

    A "trivial" value is a plain scalar, or a tuple or list consisting
    only of trivial values.

    The test name format is controlled by enum ``TestNameFormat`` as well. See
    the enum documentation for further details.
    """

    # Add zeros before index to keep order
    index = "{0:0{1}}".format(index + 1, index_len)
    # if name_fmt is TestNameFormat.INDEX_ONLY or not is_trivial(value):
    #     return "{0}_{1}".format(name, index)
    if (name_fmt is TestNameFormat.INDEX_ONLY or not is_trivial(value)) and not isinstance(value, dict):
        return "{0}_{1}".format(name, index)
    if isinstance(value, dict):
        try:
            value = value['case_name']
        except KeyError:
            return "{0}_{1}".format(name, index)
    try:
        value = str(value)
    except UnicodeEncodeError:
        # fallback for python2
        value = value.encode('ascii', 'backslashreplace')
    test_name = "{0}_{1}_{2}".format(name, index, value)
    return re.sub(r'\W|^(?=\d)', '_', test_name)


def feed_data(func, new_name, test_data_docstring, *args, **kwargs):
    """
    This internal method decorator feeds the test data item to the test.

    """

    @wraps(func)
    def wrapper(self):
        return func(self, *args, **kwargs)

    wrapper.__name__ = new_name
    wrapper.__wrapped__ = func
    # set docstring if exists
    if test_data_docstring is not None:
        wrapper.__doc__ = test_data_docstring
    else:
        # Try to call format on the docstring
        if func.__doc__:
            try:
                wrapper.__doc__ = func.__doc__.format(*args, **kwargs)
            except (IndexError, KeyError):
                # Maybe the user has added some of the formating strings
                # unintentionally in the docstring. Do not raise an exception
                # as it could be that user is not aware of the
                # formating feature.
                pass
    return wrapper


def add_test(cls, test_name, test_docstring, func, *args, **kwargs):
    """
    Add a test case to this class.

    The test will be based on an existing function but will give it a new
    name.

    """
    setattr(cls, test_name, feed_data(func, test_name, test_docstring,
                                      *args, **kwargs))


def process_file_data(cls, name, func, file_attr):
    """
    Process the parameter in the `file_data` decorator.
    """
    cls_path = os.path.abspath(inspect.getsourcefile(cls))
    data_file_path = os.path.join(os.path.dirname(cls_path), file_attr)

    def create_error_func(message):  # pylint: disable-msg=W0613
        def func(*args):
            raise ValueError(message % file_attr)

        return func

    # If file does not exist, provide an error function instead
    if not os.path.exists(data_file_path):
        test_name = mk_test_name(name, "error")
        test_docstring = """Error!"""
        add_test(cls, test_name, test_docstring,
                 create_error_func("%s does not exist"), None)
        return

    _is_yaml_file = data_file_path.endswith((".yml", ".yaml"))

    # Don't have YAML but want to use YAML file.
    if _is_yaml_file and not _have_yaml:
        test_name = mk_test_name(name, "error")
        test_docstring = """Error!"""
        add_test(
            cls,
            test_name,
            test_docstring,
            create_error_func("%s is a YAML file, please install PyYAML"),
            None
        )
        return

    with codecs.open(data_file_path, 'r', 'utf-8') as f:
        # Load the data from YAML or JSON
        if _is_yaml_file:
            if hasattr(func, YAML_LOADER_ATTR):
                yaml_loader = getattr(func, YAML_LOADER_ATTR)
                data = yaml.load(f, Loader=yaml_loader)
            else:
                data = yaml.safe_load(f)
        else:
            data = json.load(f)

    _add_tests_from_data(cls, name, func, data)


def _add_tests_from_data(cls, name, func, data):
    """
    Add tests from data loaded from the data file into the class
    """
    for i, elem in enumerate(data):
        if isinstance(data, dict):
            key, value = elem, data[elem]
            test_name = mk_test_name(name, key, i)
        elif isinstance(data, list):
            value = elem
            test_name = mk_test_name(name, value, i)
        if isinstance(value, dict):
            add_test(cls, test_name, test_name, func, **value)
        else:
            add_test(cls, test_name, test_name, func, value)


def _is_primitive(obj):
    """Finds out if the obj is a "primitive". It is somewhat hacky but it works.
    """
    return not hasattr(obj, '__dict__')


def _get_test_data_docstring(func, value):
    """Returns a docstring based on the following resolution strategy:
    1. Passed value is not a "primitive" and has a docstring, then use it.
    2. In all other cases return None, i.e the test name is used.
    """
    if not _is_primitive(value) and value.__doc__:
        return value.__doc__
    else:
        return None


def ddt(arg=None, **kwargs):
    """
    Class decorator for subclasses of ``unittest.TestCase``.

    Apply this decorator to the test case class, and then
    decorate test methods with ``@data``.

    For each method decorated with ``@data``, this will effectively create as
    many methods as data items are passed as parameters to ``@data``.

    The names of the test methods follow the pattern
    ``original_test_name_{ordinal}_{data}``. ``ordinal`` is the position of the
    data argument, starting with 1.

    For data we use a string representation of the data value converted into a
    valid python identifier.  If ``data.__name__`` exists, we use that instead.

    For each method decorated with ``@file_data('test_data.json')``, the
    decorator will try to load the test_data.json file located relative
    to the python file containing the method that is decorated. It will,
    for each ``test_name`` key create as many methods in the list of values
    from the ``data`` key.

    Decorating with the keyword argument ``testNameFormat`` can control the
    format of the generated test names.  For example:

    - ``@ddt(testNameFormat=TestNameFormat.DEFAULT)`` will be index and values.

    - ``@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)`` will be index only.

    - ``@ddt`` is the same as DEFAULT.

    """
    fmt_test_name = kwargs.get("testNameFormat", TestNameFormat.DEFAULT)

    def wrapper(cls):
        for name, func in list(cls.__dict__.items()):
            if hasattr(func, DATA_ATTR):
                for i, v in enumerate(getattr(func, DATA_ATTR)):
                    test_name = mk_test_name(
                        name,
                        getattr(v, "__name__", v),
                        i,
                        fmt_test_name
                    )
                    test_data_docstring = _get_test_data_docstring(func, v)
                    if hasattr(func, UNPACK_ATTR):
                        if isinstance(v, tuple) or isinstance(v, list):
                            add_test(
                                cls,
                                test_name,
                                test_data_docstring,
                                func,
                                *v
                            )
                        else:
                            # unpack dictionary
                            add_test(
                                cls,
                                test_name,
                                test_data_docstring,
                                func,
                                **v
                            )
                    else:
                        add_test(cls, test_name, test_data_docstring, func, v)
                delattr(cls, name)
            elif hasattr(func, FILE_ATTR):
                file_attr = getattr(func, FILE_ATTR)
                process_file_data(cls, name, func, file_attr)
                delattr(cls, name)
        return cls

    # ``arg`` is the unittest's test class when decorating with ``@ddt`` while
    # it is ``None`` when decorating a test class with ``@ddt(k=v)``.
    return wrapper(arg) if inspect.isclass(arg) else wrapper

【discover_cases.py】

from datetime import datetime
from HTMLTestRunner import HTMLTestRunner

from Conf.handle_config import do_config


# 指定路径下查找用例, 可以输入. 代表当前目录
suit = unittest.defaultTestLoader.discover(r"D:\PythonWorkFolder\Unit_Test\Unit_Test_Cases")

"""
以日期命名报告,避免覆盖
"""
str_now = datetime.strftime(datetime.now(), '%Y_%m_%d_%H_%M_%S')
report_path = do_config.get_value("path", "report_path")
# name = r"D:\PythonWorkFolder\Unit_Test\Reports\\" + str_now+"_report.html"
name = report_path + str_now + "_report.html"

report = open(name, mode='wb')
"""
verbosity 代表报告的详细程度, 0 1 2 , 2是最详细的
"""
runner = HTMLTestRunner(stream=report, title='测试数学算法的报告', verbosity=2, description='一个简单的数学算法的报告')
runner.run(suit)

【moudle_cases.py】

import unittest
from Unit_Test_Cases.test_addition_cases import AdditionCases
from Unit_Test_Cases.test_subtraction_cases import Subtraction


# 创建套件
suit = unittest.TestSuite()
# 创建加载器
load = unittest.TestLoader()
# 加载用例
suit.addTest(load.loadTestsFromModule(AdditionCases))
suit.addTest(load.loadTestsFromModule(Subtraction))

# 创建执行
runner = unittest.TextTestRunner()
# 执行用例
runner.run(suit)

【test_addition_cases.py】

import unittest
from modification_ddt.ddt import ddt, data

from Calculation_Method.Calculation import Calculation
from HandleOption.handle_excel import HandleExcel
from Conf.handle_config import do_config
from log.handle_logging import do_log

cases_path = do_config.get_value("path", "cases_excel_path")
cases_sheet = do_config.get_value("name", "add_sheet_name")
# 读取测试用例
# do_excel = HandleExcel(r'D:\PythonWorkFolder\Unit_Test\Cases_Excels\cases.xlsx', "add")
do_excel = HandleExcel(cases_path, cases_sheet)
cases = do_excel.get_cases()


@ddt()
class AdditionCases(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        """运行用例之前,打开日志文件, 这是我自己写的日志文件,注释忽略"""
        # # log_file = r'D:\PythonWorkFolder\Unit_Test\log\logfile.txt'
        # log_file = do_config.get_value("path", "log_path")
        # # mode='a'表示追加写入, w则表示覆盖
        # cls.logfile = open(log_file, mode='a', encoding='utf-8')
        # cls.logfile.write("\n{:=^40s}\n".format("开始执行用例"))  # 写入一句,开始执行用例

        # 使用封装的日志
        do_log.info("开始执行用例")

    @classmethod
    def tearDownClass(cls) -> None:
        # cls.logfile.write("\n{:=^40s}\n".format("结束执行用例"))
        # cls.logfile.close()
        do_log.info("结束执行用例")

    @data(*cases)  # 使用数据驱动循环执行此用例,每取出一条数据,当作一个用例执行
    def test_addition(self, one_case):
        num1 = one_case['num1']
        num2 = one_case['num2']
        name = one_case['case_name']
        result = Calculation(num1, num2).addition()
        pass_result = do_config.get_value("excel", "pass_result")
        fail_result = do_config.get_value("excel", "fail_result")
        try:
            self.assertEqual(one_case['expect'], result, msg="{}用例执行不通过!".format(name))
            # self.logfile.write("{}用例执行通过。\n".format(name))
            do_log.debug("{}用例执行通过。".format(name))
            do_excel.write_result(one_case['case_id'] + 1, result, pass_result)
        except AssertionError as e:
            # self.logfile.write("执行{}用例失败。\n错误原因为{}\n".format(name, e))
            do_log.error("执行{}用例失败。错误原因为{}".format(name, e))
            do_excel.write_result(one_case['case_id'] + 1, result, fail_result)
            raise e

【test_subtraction_cases.py】

import unittest
from ddt import ddt, data

from Calculation_Method.Calculation import Calculation
from HandleOption.handle_excel import HandleExcel
from Conf.handle_config import do_config
from log.handle_logging import do_log


cases_path = do_config.get_value("path", "cases_excel_path")
cases_sheet = do_config.get_value("name", "subtraction_sheet_name")
# 读取测试用例
# do_excel = HandleExcel(r'D:\PythonWorkFolder\Unit_Test\Cases_Excels\cases.xlsx', "subtraction")
do_excel = HandleExcel(cases_path, cases_sheet)
cases = do_excel.get_cases()


@ddt()
class Subtraction(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        """运行用例之前,打开日志文件"""
        # log_file = r'D:\PythonWorkFolder\Unit_Test\log\logfile.txt'
        # log_file = do_config.get_value("path", "log_path")
        # # mode='a'表示追加写入, w则表示覆盖
        # cls.logfile = open(log_file, mode='a', encoding='utf-8')
        # cls.logfile.write("\n{:=^40s}\n".format("开始执行用例"))  # 写入一句,开始执行用例
        do_log.info("开始执行用例")

    @classmethod
    def tearDownClass(cls) -> None:
        # cls.logfile.write("\n{:=^40s}\n".format("结束执行用例"))
        # cls.logfile.close()
        do_log.info("结束执行用例")

    @data(*cases)  # 使用数据驱动循环执行此用例,每取出一条数据,当作一个用例执行
    def test_addition(self, one_case):
        num1 = one_case['num1']
        num2 = one_case['num2']
        name = one_case['case_name']
        result = Calculation(num1, num2).addition()
        pass_result = do_config.get_value("excel", "pass_result")
        fail_result = do_config.get_value("excel", "fail_result")
        try:
            self.assertEqual(one_case['expect'], result, msg="{}用例执行不通过!".format(name))
            # self.logfile.write("{}用例执行通过。\n".format(name))
            do_log.debug("{}用例执行通过。".format(name))
            do_excel.write_result(one_case['case_id'] + 1, result, pass_result)
        except AssertionError as e:
            # self.logfile.write("执行{}用例失败。\n错误原因为{}\n".format(name, e))
            do_log.error("执行{}用例失败。错误原因为{}".format(name, e))
            do_excel.write_result(one_case['case_id'] + 1, result, fail_result)
            raise e

生成的报告的截图:
在这里插入图片描述
cases.xlsx中用例,最后两列则执行用例后写入:
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

单元测试简单示例:python+unittest+ddt+HTMLTestRunner+config配置文件(重在思路) 的相关文章

  • InterfaceError:连接已关闭(使用 django + celery + Scrapy)

    当我在 Celery 任务中使用 Scrapy 解析函数 有时可能需要 10 分钟 时 我得到了这个信息 我用 姜戈 1 6 5 django celery 3 1 16 芹菜 3 1 16 psycopg2 2 5 5 我也使用了psyc
  • Python PAM 模块的安全问题?

    我有兴趣编写一个 PAM 模块 该模块将利用流行的 Unix 登录身份验证机制 我过去的大部分编程经验都是使用 Python 进行的 并且我正在交互的系统已经有一个 Python API 我用谷歌搜索发现pam python http pa
  • 如何生成给定范围内的回文数列表?

    假设范围是 1 X 120 这是我尝试过的 gt gt gt def isPalindrome s check if a number is a Palindrome s str s return s s 1 gt gt gt def ge
  • 为 pandas 数据透视表中的每个值列定义 aggfunc

    试图生成具有多个 值 列的数据透视表 我知道我可以使用 aggfunc 按照我想要的方式聚合值 但是如果我不想对两列求和或求平均值 而是想要一列的总和 同时求另一列的平均值 该怎么办 那么使用 pandas 可以做到这一点吗 df pd D
  • Python tcl 未正确安装

    我刚刚为 python 安装了graphics py 但是当我尝试运行以下代码时 from graphics import def main win GraphWin My Circle 100 100 c Circle Point 50
  • __del__ 真的是析构函数吗?

    我主要用 C 做事情 其中 析构函数方法实际上是为了销毁所获取的资源 最近我开始使用python 这真的很有趣而且很棒 我开始了解到它有像java一样的GC 因此 没有过分强调对象所有权 构造和销毁 据我所知 init 方法对我来说在 py
  • keras加载模型错误尝试将包含17层的权重文件加载到0层的模型中

    我目前正在使用 keras 开发 vgg16 模型 我用我的一些图层微调 vgg 模型 拟合我的模型 训练 后 我保存我的模型model save name h5 可以毫无问题地保存 但是 当我尝试使用以下命令重新加载模型时load mod
  • 运行多个 scrapy 蜘蛛的正确方法

    我只是尝试使用在同一进程中运行多个蜘蛛新的 scrapy 文档 http doc scrapy org en 1 0 topics practices html但我得到 AttributeError CrawlerProcess objec
  • IRichBolt 在storm-1.0.0 和 pyleus-0.3.0 上运行拓扑时出错

    我正在运行风暴拓扑 pyleus verbose local xyz topology jar using storm 1 0 0 pyleus 0 3 0 centos 6 6并得到错误 线程 main java lang NoClass
  • python pandas 中的双端队列

    我正在使用Python的deque 实现一个简单的循环缓冲区 from collections import deque import numpy as np test sequence np array range 100 2 resha
  • ExpectedFailure 被计为错误而不是通过

    我在用着expectedFailure因为有一个我想记录的错误 我现在无法修复 但想将来再回来解决 我的理解expectedFailure是它会将测试计为通过 但在摘要中表示预期失败的数量为 x 类似于它如何处理跳过的 tets 但是 当我
  • Python:尝试检查有效的电话号码

    我正在尝试编写一个接受以下格式的电话号码的程序XXX XXX XXXX并将条目中的任何字母翻译为其相应的数字 现在我有了这个 如果启动不正确 它将允许您重新输入正确的数字 然后它会翻译输入的原始数字 我该如何解决 def main phon
  • 从 pygame 获取 numpy 数组

    我想通过 python 访问我的网络摄像头 不幸的是 由于网络摄像头的原因 openCV 无法工作 Pygame camera 使用以下代码就像魅力一样 from pygame import camera display camera in
  • 如何将 PIL 图像转换为 NumPy 数组?

    如何转换 PILImage来回转换为 NumPy 数组 这样我就可以比 PIL 进行更快的像素级转换PixelAccess允许 我可以通过以下方式将其转换为 NumPy 数组 pic Image open foo jpg pix numpy
  • VSCode:调试配置中的 Python 路径无效

    对 Python 和 VSCode 以及 stackoverflow 非常陌生 直到最近 我已经使用了大约 3 个月 一切都很好 当尝试在调试器中运行任何基本的 Python 程序时 弹出窗口The Python path in your
  • 在 Pandas DataFrame Python 中添加新列[重复]

    这个问题在这里已经有答案了 例如 我在 Pandas 中有数据框 Col1 Col2 A 1 B 2 C 3 现在 如果我想再添加一个名为 Col3 的列 并且该值基于 Col2 式中 如果Col2 gt 1 则Col3为0 否则为1 所以
  • 协方差矩阵的对角元素不是 1 pandas/numpy

    我有以下数据框 A B 0 1 5 1 2 6 2 3 7 3 4 8 我想计算协方差 a df iloc 0 values b df iloc 1 values 使用 numpy 作为 cov numpy cov a b I get ar
  • Python:元类属性有时会覆盖类属性?

    下面代码的结果让我感到困惑 class MyClass type property def a self return 1 class MyObject object metaclass MyClass a 2 print MyObject
  • 改变字典的哈希函数

    按照此question https stackoverflow com questions 37100390 towards understanding dictionaries 我们知道两个不同的字典 dict 1 and dict 2例
  • PyAudio ErrNo 输入溢出 -9981

    我遇到了与用户相同的错误 Python 使用 Pyaudio 以 16000Hz 录制音频时出错 https stackoverflow com questions 12994981 python error audio recording

随机推荐

  • Pyspark机器学习:模型评估(ml.Evaluation包的使用)

    Pyspark V3 2 1 本篇博客主要介绍pyspark ml Evaluation包的使用 1 概览 pyspark ml Evaluation包中的评估类主要包括以下几种如下表 类 作用 Evaluator 评估器的基类 但是这个类
  • Vmware 虚拟机提示:无法打开磁盘***.vmdk,未能锁定文件,解决办法

    虚拟机 vmware 6 5 Vmware 虚拟机提示 无法打开磁盘 vmdk 原因 未能锁定文件 解决办法如下 原因 非正常关闭虚拟机 解决办法 一 删除虚拟机文件所在文件来夹里所有以 lck 结尾的文件及文件夹 重新启动即可解决 二 如
  • 快速排序和归并排序的比较

    快速排序和归并排序的分析比较 快速排序 归并排序 设计思想 快速排序算法是在分治算法基础上设计出来的一种排序算法 从待排序序列中任选一个元素x作为哨点 以按从小到大排序为例 将所有比x大的元素放到哨点右边 将所有比x小的元素放到哨点左边 再
  • DIY多快充协议太阳能充电器!----BOOST升压电路

    上一篇文章介绍了支持三段式锂电池充电电路 使用上海如韵电子CN3791芯片的MPPT功能提高了锂电池充电过程中的能量转换效率 带来了锂电池快速蓄电 这篇文章咋们来看看如何将锂电池电压转化成支持多种快充协议的电压 单节锂电池的最高电圧为4 2
  • Python 实现 Dijkstar 路径规划算法

    Dijstar 最短路径算法 用于计算起始点到最终点的最短路径 一般采用的是贪心算法策略 原理可以参考 图解 Open list 和 close list 环境 Terminal 需要预先安装两个库 matplotlib 和 math pi
  • LeetCode题目笔记——1710. 卡车上的最大单元数

    文章目录 题目描述 题目难度 简单 方法一 贪心 代码 Python 代码 C 总结 题目描述 请你将一些箱子装在 一辆卡车 上 给你一个二维数组 boxTypes 其中 boxTypes i numberOfBoxesi numberOf
  • NanoPC-T4

    0 前言 Android源码目录frameworks native opengl tests提供了大量测试案例 本文重点分析其中的gl basic 下面先上效果图 图0 1 gl basic运行效果 此时dumpsys SurfaceFli
  • 时区(Timezone)一览表

    System out println String join TimeZone getAvailableIDs 获取指定时区当前系统时间 按时区获取当前YYYYMMDD格式日期 param timezone return public st
  • 软件架构的10个常见模式

    企业规模的软件系统该如何设计呢 在开始写代码之前 我们需要选择一个合适的架构 这个架构将决定软件实施过程中的功能属性和质量属性 因此 了解软件设计中的不同架构模式对我们的软件设计会有较大的帮助 什么是架构模式 根据维基百科 架构模式是针对特
  • vue项目使用外部字体

    1 下载字体 https www dafont com 2 项目中assets下添加一个字体样式文件夹front 将下载好的文件放到文件夹中 并创建一个front css字体样式文件 font face font family jap tr
  • Docker入门教程(详细)

    目录 一 Docker概述 1 1 Docker 为什么出现 1 2 Dorker历史 1 3 能做什么 虚拟机技术 通过 软件 模拟的具有完整 硬件 系统功能的 运行在一个完全 隔离 环境中的完整 计算机系统 容器化技术 容器化技术不是模
  • 【python办公自动化】PysimpleGUI中更新Listbox组件选定元素的格式

    pysimplegui中更新Listbox组件选定元素的格式 背景 问题解决 创建窗口布局 创建窗口 背景 在进行打分时候 由于打分的指标较多 因此为了辨别已经打完分数的指标 可以考虑对打过分的指标进行标记 故可以采用格式修改的方法调整 比
  • pandas--实战以及使用pyecharts绘图,(面向对象)

    实战9 covid approval toplines csv subject 与covid 19处理有关的 Trump modeldate 日期 party 政党 approve estimate 赞成 disapprove estima
  • matplotlib colors table/matplotlib 颜色表

    官网 https matplotlib org stable gallery color named colors html 可直接在线复制 https www kdocs cn l cnxPATUkMDCE 第一列 第二列 第三列 第四列
  • STL详解(很全)

    目录 概述 STL六大组件简介 三大组件介绍 1 容器 2 算法 3 迭代器 常用容器 1 string容器 string容器基本概念 string容器常用操作 2 vector容器 vector容器基本概念 vector迭代器 vecto
  • java调用存储过程超时及DBCP参数配置说明

    问题 生产环境实时打标超时 分析原因 实时打标java服务中 只创建数据库Connection 没有关闭数据库Connection 导致数据库连接池耗尽 无法再次获取数据库链接 解决 实时打标java服务中 增加 关闭数据库Connecti
  • 详细讲解MMU——为什么嵌入式linux没他不行?

    MMU内存管理 MMU Memory Management Unit 内存管理单元 是一种硬件模块 用于在CPU和内存之间实现虚拟内存管理 其主要功能是将虚拟地址转换为物理地址 同时提供访问权限的控制和缓存管理等功能 MMU是现代计算机操作
  • Git安装详解(写吐了,看完不后悔)

    Git 是一个非常流行的分布式版本控制系统 它帮助开发者管理和跟踪项目中的代码变化 通俗地说 可以认为 Git 就像是一个代码的时间机器 它记录了项目从开始到结束的每一次代码变动 无论你是个人开发者还是团队成员 掌握 Git 都能提高你的工
  • 2023年深圳杯A题完整版论文

    专栏内已发布ABCD篇 论文 思路 代码 订阅即可看到
  • 单元测试简单示例:python+unittest+ddt+HTMLTestRunner+config配置文件(重在思路)

    这是一个基于数学运算加减法的单元测试示例 重点在于单元测试的设计思路梳理 目的 测试数据从excel中获取 执行后并将结果写入excel 并生成报告 重点 可根据代码中的注释进行帮助理解设计思路 下面是我的测试结构 下面来解释一下各个目录及