这是一个基于数学运算加减法的单元测试示例。重点在于单元测试的设计思路梳理。
目的:测试数据从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中用例,最后两列则执行用例后写入: