Python Nose 自动化测试框架介绍

2023-05-16

文章目录

  • 1. unittest 简介
    • 1.1 python 单元测试
    • 1.2 unittest 测试框架
    • 1.3 默认模式
    • 1.4 手工模式
  • 2. nose 扩展框架
    • 2.1 `nose` 的安装和基本用法
    • 2.2 `被测对象` 的扩展
    • 2.3 `TestFixture` 的扩展
    • 2.4 nose 插件
  • 参考资料

1. unittest 简介

1.1 python 单元测试

1、python语言非常简洁和方便,一个最简单的程序 hello.py 编写如下:

class hello():
    def __init__(self):
        print("class hello init!")
        
if __name__ == "__main__":
    h = hello()

运行结果:

# python hello.py
class hello init!

2、我们使用 python 进一步开发了一些功能函数,如 demo.py 文件:

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

def add(a, b):
    return a+b

def minus(a, b):
    return a-b

如果要对这些功能函数进行单元测试,通常需要开发一系列的测试代码来进行验证:

def test_add(self):
    """Test method add(a, b)"""
    self.assertEqual(3, add(1, 2))
    self.assertNotEqual(3, add(2, 2))

def test_minus(self):
    """Test method minus(a, b)"""
    self.assertEqual(1, minus(3, 2))
    self.assertNotEqual(1, minus(3, 2))

一切看起来都很ok,但是如果我们开发了成百上千的测试代码以后,如何调用这些 test_xxx() 测试代码成了一个难题。我们不得不再开发一套框架来调用这些测试用例,幸运的是 pythonn 已经内建了这样一套框架 unittest

1.2 unittest 测试框架

在这里插入图片描述

unittest 的运行流程如上图所示:

  • 1、unittest 首先分析被测对象,找出其中的 TestCaseTestFixture ,被由 TestLoader 将其打包成 TestSuite
  • 2、然后在 TestRunner 中逐个拆包 TestSuite,并按照运行运行其中的 TestCaseTestFixture
  • 3、测试结果由 TestReporter 输出成 txt/html/xml 等格式以供分析。

这里面有一些核心概念,逐一澄清:

  • 1、被测对象unittest 把程序中所有继承了 unittest.TestCase 类 的子类,看成被测对象。而 nosetests 扩充了被测对象,文件夹、文件、模块、类、函数 都可以充当被测对象。
import unittest

class Abcd(unittest.TestCase):
    ...
  • 2、TestCase被测对象中所有以 test开头的函数都被认为是 TestCase,会被调用。
import unittest

class Abcd(unittest.TestCase):
    def test1xxx(self):
        ...
    def test2xxx(self):
        ...
  • 3、TestFixture。字面意思是测试夹具很形象的表示了它的作用,在调用 TestCase 之前需要准备测试环境,在调用 TestCase 之后需要恢复环境。在被测对象中需要使用一些关键字来定义TestFixture函数。
import unittest

class Abcd(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        ...
    @classmethod
    def tearDownClass(cls):
        ...
    def setUp(self):
        ...
    def tearDown(self):
        ...
    def test1xxx(self):
        ...
    def test2xxx(self):
        ...

# setUp():准备环境,执行每个测试用例的前置条件
# tearDown():环境还原,执行每个测试用例的后置条件
# setUpClass():必须使用@classmethod装饰器,所有case执行的前置条件,只运行一次
# tearDownClass():必须使用@classmethod装饰器,所有case运行完后只运行一次
  • 4、TestLoader。负责将 TestCaseTestFixture 打包成 TestSuite。可以使用 unittest 默认的打包方式,也可以手工进行自定义打包。
  • 5、TestRunner。负责按照对于的顺序来运行 TestCaseTestFixture。通常情况下使用 unittest.main() 来启动整个 unittest,也可以手工定制。
import unittest

unittest.main()
  • 6、TestReporter。负责将测试结果输出成 txt/html/xml 等格式。

1.3 默认模式

我们使用一个实例了来理解上述的概念,编写一个 test_demo_class.py 文件来测试 demo.py 中的函数:

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

import unittest
from demo import add, minus


class TestDemo(unittest.TestCase):
    """Test mathfuc.py"""

    @classmethod
    def setUpClass(cls):
        print ("this setupclass() method only called once.\n")

    @classmethod
    def tearDownClass(cls):
        print ("this teardownclass() method only called once too.\n")

    def setUp(self):
        print ("do something before test : prepare environment.\n")

    def tearDown(self):
        print ("do something after test : clean up.\n")

    def test_add(self):
        """Test method add(a, b)"""
        self.assertEqual(3, add(1, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """Test method minus(a, b)"""
        self.assertEqual(1, minus(3, 2))
        self.assertNotEqual(1, minus(3, 2))

    @unittest.skip("do't run as not ready")
    def test_minus_with_skip(self):
        """Test method minus(a, b)"""
        self.assertEqual(1, minus(3, 2))
        self.assertNotEqual(1, minus(3, 2))


if __name__ == '__main__':
    # verbosity=*:默认是1;设为0,则不输出每一个用例的执行结果;2-输出详细的执行结果
    unittest.main(verbosity=1)

运行结果:

# python test_demo_class.py
this setupclass() method only called once.

do something before test : prepare environment.

do something after test : clean up.

.do something before test : prepare environment.

do something after test : clean up.

Fsthis teardownclass() method only called once too.


======================================================================
FAIL: test_minus (__main__.TestDemo)
Test method minus(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\weilin.peng\Documents\python\test_demo_class.py", line 33, in test_minus
    self.assertNotEqual(1, minus(3, 2))
AssertionError: 1 == 1

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (failures=1, skipped=1)

1.4 手工模式

在上述的实例中,只要调用 unittest.main(),整个 TestLoader 打包 TestSuiteTestRunner 运行 TestSuiteTestReporter输出测试结果 都是自动进行的。

当然你也可以定制这一切,以下是一个手工定制来调用 test_demo_class.py 中测试的例子。本文件为 test_demo_module.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import HTMLReport
import unittest
import test_demo_class
from test_demo_class import TestDemo


if __name__ == '__main__':
    paras = sys.argv[1:]
    args = paras[0]
    report = paras[1]

    suite = unittest.TestSuite()
    if args == 'test':
        tests = [TestDemo("test_minus"), TestDemo("test_add"), TestDemo("test_minus_with_skip")]
        suite.addTests(tests)
    elif args == 'tests':
        suite.addTest(TestDemo("test_minus"))
        suite.addTest(TestDemo("test_add"))
        suite.addTest(TestDemo("test_minus_with_skip"))
    elif args == 'class':
        suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestDemo))
    elif args == 'module':
        suite.addTests(unittest.TestLoader().loadTestsFromModule(test_demo_class))
    elif args == 'mix':
        suite.addTests(unittest.TestLoader().loadTestsFromName('test_demo_class.TestDemo.test_minus'))
    elif args == 'mixs':
        suite.addTests(unittest.TestLoader().loadTestsFromNames(['test_demo_class.TestDemo.test_minus', 'test_demo_class.TestDemo', 'test_demo_class']))
    elif args == 'discover':
        suite.addTests(unittest.TestLoader().discover('.', 'test_*.py', top_level_dir=None))

    if report == 'terminal':
        runner = unittest.TextTestRunner(verbosity=1)
        runner.run(suite)
    elif report == 'txt':
        with open('ut_log.txt', 'a') as fp:
            runner = unittest.TextTestRunner(stream=fp, verbosity=1)
            runner.run(suite)
    elif report == 'html':
        runner = HTMLReport.TestRunner(report_file_name='test',
                               output_path='report',
                               title='测试报告',
                               description='测试描述',
                               sequential_execution=True
                               )
        runner.run(suite)

运行结果:

# python test_demo_module.py test txt
this setupclass() method only called once.

do something before test : prepare environment.

do something after test : clean up.

do something before test : prepare environment.

do something after test : clean up.

this teardownclass() method only called once too.

2. nose 扩展框架

得益于 unittest 优秀的架构,有几个框架在此之上进行了扩展,例如 nosepytest

nose 的口号是 nose extends unittest to make testing easier.,可见它是基于 unittest 之上进行的扩展,主体思想是和 unittest 一脉相承的,各个术语也是通用的。值得一提的话 nose 1 已经停止了维护,但是因为功能非常强大和好用应对一般的测试已经绰绰有余了,可以继续使用。如果介意的话,可以转用 pytest

2.1 nose 的安装和基本用法

使用 pip 安装 nose:

pip install nose

最简单的使用方法,进入工程目录执行 nosetests 命令,nose 会自动搜寻工程目录下的 TestCase 并执行:

cd path/to/project
nosetests

2.2 被测对象 的扩展

nose 一个最大的特色就是对 unittest被测对象 进行了扩展,极大的方便了测试。可以在不指定具体被测对象的情况下,nose 根据一定规则能找出工程中绝大部分的 TestCase

nose 根据以下的规则来自动查找 被测对象

  • 1、If it looks like a test, it’s a test. Names of directories, modules, classes and functions are compared against the testMatch regular expression, and those that match are considered tests. Any class that is a unittest.TestCase subclass is also collected, so long as it is inside of a module that looks like a test.

  • 1、如果它看起来像一个 test,那么它就是一个 test文件夹模块 (modules)类 (claesses)函数(functions)的名字 和 testMatch 正则表达进行比较,匹配即视为 tests。另外任何unittest.TestCase的子类也会被搜集,只要它在一个看起来像测试的模块中。

  • 2、Files with the executable bit set are ignored by default under Unix-style operating systems–use --exe to allow collection from them, but be careful that is safe to do so. Under Windows, executable files will be picked up by default since there is no executable bit to test.

  • 2、在unix风格的操作系统下,默认情况下会忽略带有可执行位集的文件,使用 --exe 选项来允许从它们收集,但是要注意这样做是安全的。在Windows下,默认情况下将选择可执行文件,因为没有可执行位要测试。

  • 3、Directories that don’t look like tests and aren’t packages are not inspected.

  • 3、看起来不像测试或不是包 (packages)的目录不会被检查。

  • 4、Packages are always inspected, but they are only collected if they look like tests. This means that you can include your tests inside of your packages (somepackage/tests) and nose will collect the tests without running package code inappropriately.

  • 4、包 (packages)总是被检查的,但是只有当其中代码看起来像测试时才会被收集。这意味着您可以在包(某些包/测试)中包含测试,nose将收集测试,而不会不适当地运行包代码。

  • 5、When a project appears to have library and test code organized into separate directories, library directories are examined first.

  • 5、当一个项目 (project)将库和测试代码组织到单独的目录中时,首先检查库目录。

  • 6、When nose imports a module, it adds that module’s directory to sys.path; when the module is inside of a package, like package.module, it will be loaded as package.module and the directory of package will be added to sys.path.

  • 6、当 nose 导入一个模块 (module)时,它会将该模块的目录添加到 sys.path;当模块在包 (packages)中,比如package.module,它将作为package.module加载,package 目录将被添加到 sys.path

  • 7、If an object defines a __test__ attribute that does not evaluate to True, that object will not be collected, nor will any objects it contains.

  • 7、如果一个对象 (object)定义了一个 __test__ 属性的值不为 True,那么该对象和它包含的任何对象都不会被收集。

  • 8、Be aware that plugins and command line options can change any of those rules.

  • 8、请注意,插件和命令行选项可以更改任何这些规则。

可以看到 nose 的默认规则极大的扩展了 被测对象,从 unittest 的一种扩展到以下的几种:

indextargetconditiondescript
0文件夹 (Directory) / 包 (packages)条件1:文件夹 name 匹配 testMatch
条件2:文件夹包含__init__.py是一个包 (packages)
满足任一条件即可
1文件 (File) / 模块 (Module)条件1:文件 name 匹配 testMatch-
2类 (claesses)条件1:类 name 匹配 testMatch
条件2:类继承了unittest.TestCase父类
满足任一条件即可
条件2是unittest唯一支持的
3函数 (functions)条件1:函数 name 匹配 testMatch-

这里面的一个关键就是 testMatch 正则表达式,nosetests --help 会打印出 testMatch 的默认值:

-m=REGEX, --match=REGEX, --testmatch=REGEX¶
Files, directories, function names, and class names that match this regular expression are considered tests. Default: (?:\b|_)[Tt]est [NOSE_TESTMATCH]

可以看到testMatch 的默认值 为 (?:\b|_)[Tt]est,解析正则表达式的具体含义:

在这里插入图片描述

2.3 TestFixture 的扩展

nose 支持各个层级的 TestFixture,在每个层级都能进行 测试场景部署 和 原场景还原 的动作。

indextargetfixture keyworddescript
0Test packagessetup()/setup_package()/setUp()/setUpPackage()
teardown()/teardown_package()/tearDown()/tearDownPackage()
在测试包/文件夹中的 __init__.py 文件中定义
1Test modulessetup()/setup_module()/setUp()/setUpModule()
teardown()/teardown_module()/tearDownModule()
-
2Test classessetup_class()/setupClass()/setUpClass()/setupAll()/setUpAll()
teardown_class()/teardownClass()/tearDownClass()/teardownAll()/tearDownAll()
必须使用 @classmethod 声明成类方法
3Test functionssetup()/setup_func()
teardown()/teardown_func()
可以使用类似方法给函数分配 fixture 函数test = with_setup(setup_func, teardown_func)(test)

以下是一个基本的例子来展示不同层次 TestFixture 的不同执行时机。

  • 1、unnecessary_math.py:
'''
Module showing how doctests can be included with source code
Each '>>>' line is run as if in a python shell, and counts as a test.
The next line, if not '>>>' is the expected output of the previous line.
If anything doesn't match exactly (including trailing spaces), the test fails.
'''

def multiply(a, b):
    """
    >>> multiply(4, 3)
    12
    >>> multiply('a', 3)
    'aaa'
    """
    return a * b
  • 2、test_um_nose_fixtures.py:
from nose import with_setup # optional
from unnecessary_math import multiply

def setup_module(module):
    print ("") # this is to get a newline after the dots
    print ("setup_module before anything in this file")

def teardown_module(module):
    print ("teardown_module after everything in this file")

def my_setup_function():
    print ("my_setup_function")

def my_teardown_function():
    print ("my_teardown_function")

@with_setup(my_setup_function, my_teardown_function)
def test_numbers_3_4():
    print ('test_numbers_3_4  <============================ actual test code')
    assert multiply(3,4) == 12

@with_setup(my_setup_function, my_teardown_function)
def test_strings_a_3():
    print ('test_strings_a_3  <============================ actual test code')
    assert multiply('a',3) == 'aaa'

class TestUM:
    @classmethod
    def setup_class(cls):
        print ("setup_class() before any methods in this class")

    @classmethod
    def teardown_class(cls):
        print ("teardown_class() after any methods in this class")

    def setup(self):
        print ("TestUM:setup() before each test method")

    def teardown(self):
        print ("TestUM:teardown() after each test method")

    def test_numbers_5_6(self):
        print ('test_numbers_5_6()  <============================ actual test code')
        assert multiply(5,6) == 30

    def test_strings_b_2(self):
        print ('test_strings_b_2()  <============================ actual test code')
        assert multiply('b',2) == 'bb'
  • 3、运行结果:
# nosetests -s test_um_nose_fixtures.py

setup_module before anything in this file
setup_class() before any methods in this class
TestUM:setup() before each test method
test_numbers_5_6()  <============================ actual test code
TestUM:teardown() after each test method
.TestUM:setup() before each test method
test_strings_b_2()  <============================ actual test code
TestUM:teardown() after each test method
.teardown_class() after any methods in this class
my_setup_function
test_numbers_3_4  <============================ actual test code
my_teardown_function
.my_setup_function
test_strings_a_3  <============================ actual test code
my_teardown_function
.teardown_module after everything in this file

----------------------------------------------------------------------
Ran 4 tests in 0.012s

OK

2.4 nose 插件

nose 还提供了丰富的内部和外部插件,并且可以自己开发插件。来使测试更加智能和方便。

关于这部分可以重点参考以下文档:Believer007 nose 系列文章、nose 1.3.7 documentation

参考资料

1.unittest — Unit testing framework
2.Python 单元测试 - unittest
3.Python 单元测试 - HTML report
4.Python 单元测试 - pytest
5.Believer007 nose 系列文章
6.nose Installation and quick start
7.nose 1.3.7 documentation
8.nose introduction

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

Python Nose 自动化测试框架介绍 的相关文章

  • 安装了 32 位的 Python,显示为 64 位

    我需要运行 32 位版本的 Python 我认为这就是我在我的机器上运行的 因为这是我下载的安装程序 当我重新运行安装程序时 它会将当前安装的 Python 版本称为 Python 3 5 32 位 然而当我跑步时platform arch
  • 将html数据解析成python列表进行操作

    我正在尝试读取 html 网站并提取其数据 例如 我想查看公司过去 5 年的 EPS 每股收益 基本上 我可以读入它 并且可以使用 BeautifulSoup 或 html2text 创建一个巨大的文本块 然后我想搜索该文件 我一直在使用
  • Python getstatusoutput 替换不返回完整输出

    我发现了这个很棒的替代品getstatusoutput Python 2 中的函数在 Unix 和 Windows 上同样有效 不过我觉得这个方法有问题output被构建 它只返回输出的最后一行 但我不明白为什么 任何帮助都是极好的 def
  • Python zmq SUB 套接字未接收 MQL5 Zmq PUB 套接字

    我正在尝试在 MQL5 中设置一个 PUB 套接字 并在 Python 中设置一个 SUB 套接字来接收消息 我在 MQL5 中有这个 include
  • 独立滚动矩阵的行

    我有一个矩阵 准确地说 是 2d numpy ndarray A np array 4 0 0 1 2 3 0 0 5 我想滚动每一行A根据另一个数组中的滚动值独立地 r np array 2 0 1 也就是说 我想这样做 print np
  • 使用字典映射数据帧索引

    为什么不df index map dict 工作就像df column name map dict 这是尝试使用index map的一个小例子 import pandas as pd df pd DataFrame one A 10 B 2
  • 立体太阳图 matplotlib 极坐标图 python

    我正在尝试创建一个与以下类似的简单的立体太阳路径图 http wiki naturalfrequent com wiki Sun Path Diagram http wiki naturalfrequency com wiki Sun Pa
  • Pandas Merge (pd.merge) 如何设置索引和连接

    我有两个 pandas 数据框 dfLeft 和 dfRight 以日期作为索引 dfLeft cusip factorL date 2012 01 03 XXXX 4 5 2012 01 03 YYYY 6 2 2012 01 04 XX
  • 如何在 Python 中解析和比较 ISO 8601 持续时间? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在寻找一个 Python v2 库 它允许我解析和比较 ISO 8601 持续时间may处于不同单
  • Python beautifulsoup 仅限 1 级文本

    我看过其他 beautifulsoup 得到相同级别类型的问题 看来我的有点不同 这是网站 我正试图拿到右边那张桌子 请注意表的第一行如何展开为该数据的详细细分 我不想要那个数据 我只想要最顶层的数据 您还可以看到其他行也可以展开 但在本例
  • 从Python中的字典列表中查找特定值

    我的字典列表中有以下数据 data I versicolor 0 Sepal Length 7 9 I setosa 0 I virginica 1 I versicolor 0 I setosa 1 I virginica 0 Sepal
  • 如何在不丢失注释和格式的情况下更新 YAML 文件 / Python 中的 YAML 自动重构

    我想在 Python 中更新 YAML 文件值 而不丢失 Python 中的格式和注释 例如我想改造 YAML 文件 value 456 nice value to value 6 nice value 界面类似于 y yaml load
  • pip 列出活动 virtualenv 中的全局包

    将 pip 从 1 4 x 升级到 1 5 后pip freeze输出我的全局安装 系统 软件包的列表 而不是我的 virtualenv 中安装的软件包的列表 我尝试再次降级到 1 4 但这并不能解决我的问题 这有点类似于这个问题 http
  • 从 NumPy ndarray 中选择行

    我只想从 a 中选择某些行NumPy http en wikipedia org wiki NumPy基于第二列中的值的数组 例如 此测试数组的第二列包含从 1 到 10 的整数 gt gt gt test numpy array nump
  • 仅第一个加载的 Django 站点有效

    我最近向 stackoverflow 提交了一个问题 标题为使用mod wsgi在apache上多次请求后Django无限加载 https stackoverflow com questions 71705909 django infini
  • 如何使用原始 SQL 查询实现搜索功能

    我正在创建一个由 CS50 的网络系列指导的应用程序 这要求我仅使用原始 SQL 查询而不是 ORM 我正在尝试创建一个搜索功能 用户可以在其中查找存储在数据库中的书籍列表 我希望他们能够查询 书籍 表中的 ISBN 标题 作者列 目前 它
  • Pandas 将多行列数据帧转换为单行多列数据帧

    我的数据框如下 code df Car measurements Before After amb temp 30 268212 26 627491 engine temp 41 812730 39 254255 engine eff 15
  • 实现 XGboost 自定义目标函数

    我正在尝试使用 XGboost 实现自定义目标函数 在 R 中 但我也使用 python 所以有关 python 的任何反馈也很好 我创建了一个返回梯度和粗麻布的函数 它工作正常 但是当我尝试运行 xgb train 时它不起作用 然后 我
  • Pandas 每周计算重复值

    我有一个Dataframe包含按周分组的日期和 ID df date id 2022 02 07 1 3 5 4 2022 02 14 2 1 3 2022 02 21 9 10 1 2022 05 16 我想计算每周有多少 id 与上周重
  • 在 JavaScript 函数的 Django 模板中转义字符串参数

    我有一个 JavaScript 函数 它返回一组对象 return Func id name 例如 我在传递包含引号的字符串时遇到问题 Dr Seuss ABC BOOk 是无效语法 I tried name safe 但无济于事 有什么解

随机推荐