Python—Unittest单元测试框架基础教程

文档:unittest单元测试框架
源代码:Lib/unittest/init.py
安装: pip install unittest
一.unittest概念

unittest 单元测试框架是受到 JUnit 的启发,与其他语言中的主流单元测试框架有着相似的风格。其支持测试自动化,配置共享和关机代码测试。支持将测试样例聚合到测试集中,并将测试与报告框架独立。

二.属性导图

在这里插入图片描述

三.核心组件
  1. test fixture:测试装置(执行测试用例前后的环境准备工作;例如:创建临时或代理的数据库、目录,再或者启动一个服务器进程。)
  2. testcase:测试用例(新建测试用例。)
  3. testsuit:测试套件(归档需要一起执行的测试;组合一系列测试用例。)
  4. test runner:测试运行器(执行和输出测试结果的组件;使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。)
四.属性方法
  1. 创建测试用例:

    写一个类继承 unittest.TestCase 就创建了一个测试样例。类方法的命名都以 test 开头。 这个命名约定告诉测试运行者类的哪些方法表示测试。

  2. 断言:

    [1] 官方文档:断言方法
    [2] 常用断言

    序号 断言方法 断言描述
    1 assertEqual(arg1, arg2, msg=None) 验证arg1==arg2,不等则fail
    2 assertNotEqual(arg1, arg2, msg=None) 验证arg1 != arg2, 相等则fail
    3 assertTrue(expr, msg=None) 验证expr是true,如果为false,则fail
    4 assertFalse(expr,msg=None) 验证expr是false,如果为true,则fail
    5 assertIs(arg1, arg2, msg=None) 验证arg1、arg2是同一个对象,不是则fail
    6 assertIsNot(arg1, arg2, msg=None) 验证arg1、arg2不是同一个对象,是则fail
    7 assertIsNone(expr, msg=None) 验证expr是None,不是则fail
    8 assertIsNotNone(expr, msg=None)) 验证expr不是None,是则fail
    9 assertIn(arg1, arg2, msg=None)) 验证arg1是arg2的子串,不是则fail
    10 assertNotIn(arg1, arg2, msg=None) 验证arg1不是arg2的子串,是则fail
    11 assertIsInstance(obj, cls, msg=None) 验证obj是cls的实例,不是则fail
    12 assertNotIsInstance(obj, cls, msg=None) 验证obj不是cls的实例,是则fail
  3. setUp() / tearDown() :

    可以设置测试方法开始前与完成后需要执行的指令;若 setUp() 方法引发异常,测试框架会认为测试发生了错误,因此测试方法不会被运行。若 setUp() 成功运行,无论测试方法是否成功,都会运行 tearDown() 。这样的一个测试代码运行的环境被称为 test fixture 。

  4. setUpClass()/tearDownClass():

    在运行单个类中的测试前后调用的类方法。 setUpClass/tearDownClass以类作为唯一参数调用,并且必须修饰为classmethod():

  5. unittest.main() :

    扫描二维码关注公众号,回复: 12687152 查看本文章

    提供了一个测试脚本的命令行接口。当在命令行运行该测试脚本,上文的脚本格式的输出。

  6. 跳过测试:

    [1] @unittest.skip(reason):跳过被此装饰器装饰的测试。 reason 为测试被跳过的原因。
    [2] @unittest.skipIf(condition, reason):当 condition 为真时,跳过被装饰的测试。
    [3] @unittest.skipUnless(condition, reason):跳过被装饰的测试,除非 condition 为真。
    [4] @unittest.expectedFailure:将测试标记为预期的失败或错误。如果测试失败或出错,将被视为成功。如果测试通过,将被视为失败。
    [5] exception unittest.SkipTest(reason):引发此异常以跳过一个测试。通常来说,你可以使用 TestCase.skipTest() 或其中一个跳过测试的装饰器实现跳过测试的功能,而不是直接引发此异常。
    [注意]:被跳过的测试的 setUp() 和 tearDown() 不会被运行。被跳过的类的 setUpClass() 和 tearDownClass() 不会被运行。被跳过的模组的 setUpModule() 和 tearDownModule() 不会被运行。

  7. 检查异常、警告和日志消息的生成:

    序号 方法 描述
    1 assertRaises(exc, fun, *args, **kwds) fun(*args, **kwds)提高EXC
    2 assertRaisesRegex(exc, r, fun, *args, **kwds) fun(*args, **kwds)提高EXC
    3 assertWarns(warn, fun, *args, **kwds) fun(*args, **kwds)提出警告
    4 assertWarnsRegex(warn, r, fun, *args, **kwds) fun(*args, **kwds)引发警告 ,并且消息匹配正则表达式r
    5 assertLogs(logger, level) 该with块 以最低级别登录到记录器

    注:详情请参考官方文档

  8. 测试框架可以使用以下方法来收集有关测试的信息

    [1]countTestCases():返回此测试对象表示的测试数量。对于 TestCase情况下,这将永远是1。
    [2]defaultTestResult():返回应用此测试用例类的测试结果类的实例,如果未向该run()方法提供其他结果实例 ;对于TestCase情况 下,这将永远是一个实例 TestResult; 的子类TestCase应在必要时覆盖此子类。
    [3]id():返回标识特定测试用例的字符串。这通常是测试方法的全名,包括模块和类名。
    [4]shortDescription():返回测试的描述;或者None没有提供描述。此方法的默认实现返回测试方法第一行。
    [5] …

  9. 分组测试:

    [0]unittest.TestSuite(tests =()): 此类表示各个测试用例和测试套件的集合。该类提供测试运行程序所需的接口,以使其能够像其他任何测试用例一样运行。运行TestSuite实例与遍历套件(分别运行每个测试)相同。
    [1]addTest(测试): 将TestCase或添加TestSuite到套件。
    [2]addTests(测试): 将来自所有可迭代的TestCase和TestSuite 实例的测试添加到该测试套件中。
    [3]run(结果): 运行与此套件相关的测试,将结果收集到作为result传递的测试结果对象中。请注意,不同于 TestCase.run(),TestSuite.run()需要传递结果对象。
    [4]countTestCases(): 返回此测试对象表示的测试数量,包括所有单个测试和子套件。

  10. 协程,信号处理,加载运行等方法请参考官方文档。

五.基本实例

一. 测试方法

# Test_func_demo.py

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


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


def multi(a, b):
    return a * b


def divide(a, b):
    return a / b

二.简单实例

# Test_unittest_demo.py
import unittest
from Test_func_demo import *


# 继承 unittest.TestCase 就创建了一个测试样例
class TestFun(unittest.TestCase):

    def test_add(self):
        """验证加法"""
        self.assertEqual(4, add(2, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """验证减法"""
        self.assertEqual(1, minus(2, 1))

    def test_multi(self):
        """验证乘法"""
        self.assertEqual(8, multi(2, 4))

    def test_divide(self):
        self.assertEqual(2, divide(10, 5))
        self.assertEqual(3, divide(10, 3))


if __name__ == '__main__':
    # 测试脚本的命令行接口/通过传递verbosity=2参数来运行包含更详细信息的测试,默认为0,级别共分为0,1,2.
    unittest.main(verbosity=2)

三.测试结果
在这里插入图片描述

  1. 成功.|失败F|错误E|跳过S| 注意:测试顺序并不是自上而下

四.测试套件

1.测试套件用法
注意:多个测试运行的顺序由内置字符串排序(0-9,A-Z,a-z)方法对测试名进行排序的结果决定。并不是自上而下,为了解决这一特性,利用TestSuit().

# Test_TestSuit_demo.py
import unittest
from Test_unittest_demo import TestFun

if __name__ == '__main__':
    # 创建测试套件对象
    suite = unittest.TestSuite()

    # 添加单个测试用例到套件
    # suite.addTest(TestFun('test_add'))
    # suite.addTest(TestFun('test_minus'))
    # suite.addTest(TestFun('test_divide'))

    # 将测试用例集添加到测试套件中,参数必须为list/tuple.
    tests = [TestFun('test_add'), TestFun('test_minus'), TestFun('test_divide')]
    suite.addTests(tests)

    # 注意!:只有依次添加单个或添加测试集会有序的执行测试;TestLoader的添加方法是无序的执行!
    # 用模块名+类名添加测试   /  addTests+TestLoader
    # suite.addTests(unittest.TestLoader().loadTestsFromName('Test_unittest_demo.TestFun'))
    # suite.addTests(unittest.TestLoader().loadTestsFromNames(['Test_unittest_demo.TestFun']))
    # 从测试用例加载测试     /   loadTestsFromTestCase(),传入TestCase
    # suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestFun))

    # 创建运行测试用例输出流对象
    runner = unittest.TextTestRunner(verbosity=2)
    # 运行测试套件 参数为测试套件对象
    runner.run(suite)

------测试结果:
在这里插入图片描述

2.将结果输出至文件

# Test_TestSuit_demo.py
import unittest
from Test_unittest_demo import TestFun

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestFun))

    # 将结果写入文件
    with open('unittestTextReport.txt', 'a') as f:
        # 如果stream 是None,则将默认值sys.stderr用作输出流。
        runner = unittest.TextTestRunner(stream=f, verbosity=2)
        runner.run(suite)

------ 输出结果
在这里插入图片描述

3.将测试用例与测试套件模块分离的优点:
- 可以从命令行独立运行测试模块。
- 测试代码可以更容易地与出厂代码分开。
- 在没有充分理由的情况下,很少有必要更改测试代码以适合其测试的代码。
- 测试代码的修改频率应低于其测试代码。
- 经过测试的代码可以更容易地重构。
- 无论如何,用C编写的模块的测试都必须在单独的模块中,所以为什么不一致?
- 如果测试策略发生变化,则无需更改源代码。

五.测试装置

1.setUp() / tearDown() :
在调用测试方法前后立即调用该方法。

# 重写Test_unittest_demo.py

import unittest
from Test_func_demo import *


# 继承 unittest.TestCase 就创建了一个测试样例
class TestFun(unittest.TestCase):

    def setUp(self):
        print('每个测试执行<前>的操作:连接数据库...')

    def tearDown(self):
        print('每个测试执行<后>的操作:断开连接数据库...')

    def test_add(self):
        """验证加法"""
        print('+')
        self.assertEqual(4, add(2, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """验证减法"""
        print('-')
        self.assertEqual(1, minus(2, 1))

    def test_multi(self):
        """验证乘法"""
        print('*')
        self.assertEqual(8, multi(2, 4))

    def test_divide(self):
        """验证除法"""
        print('/')
        self.assertEqual(3, divide(10, 3))


if __name__ == '__main__':
    # 测试脚本的命令行接口/通过传递verbosity参数来运行包含更详细信息的测试 0/默认:1/2
    unittest.main(verbosity=2)

------ 输出结果:

注意!可能会出现进程或其他原因导致测试结果重复出现问题。我是重启Pycharm解决的,小伙伴发现解决方案咱们评论区见。

在这里插入图片描述

2.setUpClass()/tearDownClass():
在执行所有测试前后调用的类方法。 以类作为唯一参数调用,并且必须修饰为classmethod():

# 重写Test_unittest_demo.py

import unittest
from Test_func_demo import *


# 继承 unittest.TestCase 就创建了一个测试样例
class TestFun(unittest.TestCase):

    @classmethod  # 必须修饰为类方法
    def setUpClass(cls):
        print('<所有>测试用例执行<前>的操作--此方法调用一次')

    @classmethod
    def tearDownClass(cls):
        print('<所有>测试用例执行<后>的操作--此方法调用一次')

    def setUp(self):
        print('每个测试执行<前>的操作:连接数据库...')

    def tearDown(self):
        print('每个测试执行<后>的操作:断开连接数据库...')

    def test_add(self):
        """验证加法"""
        print('+')
        self.assertEqual(4, add(2, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """验证减法"""
        print('-')
        self.assertEqual(1, minus(2, 1))

    def test_multi(self):
        """验证乘法"""
        print('*')
        self.assertEqual(8, multi(2, 4))

    def test_divide(self):
        """验证除法"""
        print('/')
        self.assertEqual(3, divide(10, 3))


if __name__ == '__main__':
    # 测试脚本的命令行接口/通过传递verbosity参数来运行包含更详细信息的测试 0/默认:1/2
    unittest.main(verbosity=2)

------ 输出结果:
在这里插入图片描述

六.跳过测试

1.无条件skip()跳过测试

import unittest
from Test_func_demo import *


# 继承 unittest.TestCase 就创建了一个测试样例
class TestFun(unittest.TestCase):

    @classmethod  # 必须修饰为类方法
    def setUpClass(cls):
        print('<所有>测试用例执行<前>的操作--此方法调用一次')

    @classmethod
    def tearDownClass(cls):
        print('<所有>测试用例执行<后>的操作--此方法调用一次')

    def setUp(self):
        print('每个测试执行<前>的操作:连接数据库...')

    def tearDown(self):
        print('每个测试执行<后>的操作:断开连接数据库...')

    def test_add(self):
        """验证加法"""
        print('+')
        self.assertEqual(4, add(2, 2))
        self.assertNotEqual(3, add(2, 2))

    def test_minus(self):
        """验证减法"""
        print('-')
        self.assertEqual(1, minus(2, 1))

    def test_multi(self):
        """验证乘法"""
        print('*')
        self.assertEqual(8, multi(2, 4))

    ######################################################################
    @unittest.skip('无条件跳过除法测试用例')
    def test_divide(self):
        """验证除法"""
        print('/')
        self.assertEqual(3, divide(10, 3))
    ######################################################################


if __name__ == '__main__':
    # 测试脚本的命令行接口/通过传递verbosity参数来运行包含更详细信息的测试 0/默认:1/2
    unittest.main(verbosity=2)

------- 输出结果:

在这里插入图片描述

2.TestCase.skipTest():也可以跳过测试。可以用于所需资源不可用的情况下跳过接下来的测试。

# 重写Test_unittest_demo.py

import unittest
from Test_func_demo import *


# 继承 unittest.TestCase 就创建了一个测试样例
class TestFun(unittest.TestCase):

   @classmethod  # 必须修饰为类方法
   def setUpClass(cls):
       print('<所有>测试用例执行<前>的操作--此方法调用一次')

   @classmethod
   def tearDownClass(cls):
       print('<所有>测试用例执行<后>的操作--此方法调用一次')

   def setUp(self):
       print('每个测试执行<前>的操作:连接数据库...')

   def tearDown(self):
       print('每个测试执行<后>的操作:断开连接数据库...')

   def test_add(self):
       """验证加法"""
       print('+')
       self.assertEqual(4, add(2, 2))
       self.assertNotEqual(3, add(2, 2))

   def test_minus(self):
       """验证减法"""
       print('-')
       self.assertEqual(1, minus(2, 1))

   def test_multi(self):
       """验证乘法"""
       # -----------------------------------------------------------------
       self.skipTest('跳过验证乘法测试')
       # -----------------------------------------------------------------
       print('*')
       self.assertEqual(8, multi(2, 4))

   ######################################################################
   @unittest.skip('无条件跳过除法测试用例')
   def test_divide(self):
       """验证除法"""
       print('/')
       self.assertEqual(3, divide(10, 3))
   ######################################################################


if __name__ == '__main__':
   # 测试脚本的命令行接口/通过传递verbosity参数来运行包含更详细信息的测试 0/默认:1/2
   unittest.main(verbosity=2)


------ 输出结果:
在这里插入图片描述

3.更多跳过测试方法:
@unittest.skip(reason)
跳过被此装饰器装饰的测试。 reason 为测试被跳过的原因。
@unittest.skipIf(condition, reason)
当 condition 为真时,跳过被装饰的测试。
@unittest.skipUnless(condition, reason)
跳过被装饰的测试,除非 condition 为真。
@unittest.expectedFailure
将测试标记为预期的失败或错误。如果测试失败或出错,将被视为成功。如果测试通过,将被视为失败。
我就不一一演示了,各位可以根据自身业务需求,灵活运用。

七.HTML生成报告

官方版本参考:http://tungwaiyip.info/software/HTMLTestRunner.html
当然,测试报告生成HTML有好几种方法,你也可以重写。

1.安装html-testRunner模块:

pip install html-testRunner -i https://pypi.tuna.tsinghua.edu.cn/simple/
# 如果安装失败提示升级:
Python  -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple/

2.代码示例

#Test_TestSuit_demo.py

import unittest
from Test_unittest_demo import TestFun
import HtmlTestRunner

if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestFun))

    with open('HTMLReport.html', 'w') as f:
        runner = HtmlTestRunner.HTMLTestRunner(
            output='./',
            stream=f,
            report_title='测试报告',
            descriptions='测试报告详情',
            verbosity=2
        )
        runner.run(suite)

------ 输出结果:
在这里插入图片描述

说明:可以看到HTML产出的报告有乱码问题,对于此模块我没有做深耕,感兴趣可继续研究下。比如实现动态加载到Django项目的模板中。

七.优点缺点

优点:

1.开发人员并不需要安装任何其他的模块。
2.UnitTest是xUnit的衍生产品,其工作原理与其他xUnit框架十分类似。因此对于那些没有过硬Python背景的人来说,也能很快地上手。
3.用户能够以更为简单的方式运行单个测试用例。您只需在终端上预定好名称,该框架便可灵活地执行各种用例的测试,并产生精炼的输出。
4.它能够在几毫秒内生成各种测试报告。

缺点

1.虽然该框架常用snake_case来命名各种Python代码,但是由于它源自Junit,因此仍保留了一些传统的camelCase命名方法。这往往会让人产生混淆。
2.由于它过多地支持了抽象方法,因此造成了测试代码的目的有时不够清晰。
3.需要大量的样板代码。

至此unittest单元测试框架基础教程完结
如有错误及疏漏,欢迎留言.

猜你喜欢

转载自blog.csdn.net/weixin_44053341/article/details/113741466