python 单元测试框架-unittest、mock

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

为了实现这些,unittest 通过面向对象的方式支持了一些重要的概念。

  • 测试脚手架

test fixture 表示为了开展一项或多项测试所需要进行的准备工作,以及所有相关的清理操作。举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。

  • 测试用例

一个测试用例是一个独立的测试单元。它检查输入特定的数据时的响应。 unittest 提供一个基类: TestCase ,用于新建测试用例。

  • 测试套件

test suite 是一系列的测试用例,或测试套件,或两者皆有。它用于归档需要一起执行的测试。

  • 测试运行器(test runner)

test runner 是一个用于执行和输出测试结果的组件。这个运行器可能使用图形接口、文本接口,或返回一个特定的值表示运行测试的结果。

基本实例

unittest 模块提供了一系列创建和运行测试的工具。这一段落演示了这些工具的一小部分,但也足以满足大部分用户的需求。

这是一段简短的代码,来测试三种字符串方法:

import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()

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

每个测试的关键是:调用 assertEqual() 来检查预期的输出; 调用 assertTrue() 或 assertFalse() 来验证一个条件;调用 assertRaises() 来验证抛出了一个特定的异常。使用这些方法而不是 assert 语句是为了让测试运行者能聚合所有的测试结果并产生结果报告。

通过 setUp() 和 tearDown() 方法,可以设置测试开始前与完成后需要执行的指令。 在 组织你的测试代码 中,对此有更为详细的描述。

最后的代码块中,演示了运行测试的一个简单的方法。 unittest.main() 提供了一个测试脚本的命令行接口。当在命令行运行该测试脚本,上文的脚本生成如以下格式的输出:

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

在调用测试脚本时添加 -v 参数使 unittest.main() 显示更为详细的信息,生成如以下形式的输出:

test_isupper (__main__.TestStringMethods) ... ok
test_split (__main__.TestStringMethods) ... ok
test_upper (__main__.TestStringMethods) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

以上例子演示了 unittest 中最常用的、足够满足许多日常测试需求的特性。文档的剩余部分详述该框架的完整特性。

命令行界面

unittest 模块可以通过命令行运行模块、类和独立测试方法的测试:

python -m unittest test_module1 test_module2
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method

你可以传入模块名、类或方法名或他们的任意组合。

同样的,测试模块可以通过文件路径指定:

python -m unittest tests/test_something.py

这样就可以使用 shell 的文件名补全指定测试模块。所指定的文件仍需要可以被作为模块导入。路径通过去除 '.py' 、把分隔符转换为 '.' 转换为模块名。若你需要执行不能被作为模块导入的测试文件,你需要直接执行该测试文件。

在运行测试时,你可以通过添加 -v 参数获取更详细(更多的冗余)的信息。

python -m unittest -v test_module

当运行时不包含参数,开始 探索性测试

python -m unittest

探索性测试

Unittest支持简单的测试搜索。若需要使用探索性测试,所有的测试文件必须是 modules 或 packages (包括 namespace packages )并可从项目根目录导入(即它们的文件名必须是有效的 identifiers )。

探索性测试在 TestLoader.discover() 中实现,但也可以通过命令行使用。它在命令行中的基本用法如下:

cd project_directory
python -m unittest discover

注:方便起见, python -m unittest 与 python -m unittest discover 等价。如果你需要向探索性测试传入参数,必须显式地使用 discover 子命令。

discover 有以下选项:

  • -v--verbose    更详细地输出结果。
  • -s--start-directory directory    开始进行搜索的目录(默认值为当前目录 . )。
  • -p--pattern pattern    用于匹配测试文件的模式(默认为 test*.py )。
  • -t--top-level-directory directory    指定项目的最上层目录(通常为开始时所在目录)。

-s ,-p 和 -t 选项可以按顺序作为位置参数传入。以下两条命令是等价的:

python -m unittest discover -s project_directory -p "*_test.py"
python -m unittest discover project_directory "*_test.py"

正如可以传入路径那样,传入一个包名作为起始目录也是可行的,如 myproject.subpackage.test 。你提供的包名会被导入,它在文件系统中的位置会被作为起始目录。

组织你的测试代码

单元测试的构建单位是 test cases :独立的、包含执行条件与正确性检查的方案。在 unittest 中,测试用例表示为 unittest.TestCase 的实例。通过编写 TestCase 的子类或使用 FunctionTestCase 编写你自己的测试用例。

一个 TestCase 实例的测试代码必须是完全自含的,因此它可以独立运行,或与其它任意组合任意数量的测试用例一起运行。

TestCase 的最简单的子类需要实现一个测试方法(例如一个命名以 test 开头的方法)以执行特定的测试代码:

import unittest

class DefaultWidgetSizeTestCase(unittest.TestCase):
    def test_default_widget_size(self):
        widget = Widget('The widget')
        self.assertEqual(widget.size(), (50, 50))

可以看到,为了进行测试,我们使用了基类 TestCase 提供的其中一个 assert*() 方法。若测试不通过,将会引发一个带有说明信息的异常,并且 unittest 会将这个测试用例标记为测试不通过。任何其它类型的异常将会被当做错误处理。

可能同时存在多个前置操作相同的测试,我们可以把测试的前置操作从测试代码中拆解出来,并实现测试前置方法 setUp() 。在运行测试时,测试框架会自动地为每个单独测试调用前置方法。

import unittest

class WidgetTestCase(unittest.TestCase):
    def setUp(self):
        self.widget = Widget('The widget')

    def test_default_widget_size(self):
        self.assertEqual(self.widget.size(), (50,50),
                         'incorrect default size')

    def test_widget_resize(self):
        self.widget.resize(100,150)
        self.assertEqual(self.widget.size(), (100,150),
                         'wrong size after resize')

注:多个测试运行的顺序由内置字符串排序方法对测试名进行排序的结果决定。

在测试运行时,若 setUp() 方法引发异常,测试框架会认为测试发生了错误,因此测试方法不会被运行。

相似的,我们提供了一个 tearDown() 方法在测试方法运行后进行清理工作。

import unittest

class WidgetTestCase(unittest.TestCase):
    def setUp(self):
        self.widget = Widget('The widget')

    def tearDown(self):
        self.widget.dispose()

若 setUp() 成功运行,无论测试方法是否成功,都会运行 tearDown() 。

这样的一个测试代码运行的环境被称为 test fixture 。一个新的 TestCase 实例作为一个测试脚手架,用于运行各个独立的测试方法。在运行每个测试时,setUp() 、tearDown() 和 __init__() 会被调用一次。

跳过测试与预计的失败

Unittest支持跳过单个测试方法甚至整个测试类别。 此外,它支持将测试标记为“预期失败”,即已损坏且将失败的测试,但不应将其视为TestResult上的失败。

跳过测试只需使用skip()装饰器或其条件变量之一,即可在setUp()或测试方法中调用TestCase.skipTest()或直接提高SkipTest。

以下装饰器和异常实现了测试跳过和预期的失败:

  • @unittest.skip(reason)    跳过被此装饰器装饰的测试。 reason 为测试被跳过的原因。
  • @unittest.skipIf(conditionreason)    当 condition 为真时,跳过被装饰的测试。
  • @unittest.skipUnless(conditionreason)    跳过被装饰的测试,除非 condition 为真。
  • @unittest.expectedFailure    把测试标记为预计失败。如果测试不通过,会被认为测试成功;如果测试通过了,则被认为是测试失败。
  • exception unittest.SkipTest(reason)    引发此异常以跳过一个测试。

通常来说,你可以使用 TestCase.skipTest() 或其中一个跳过测试的装饰器实现跳过测试的功能,而不是直接引发此异常。

被跳过的测试的 setUp() 和 tearDown() 不会被运行。被跳过的类的 setUpClass() 和 tearDownClass() 不会被运行。被跳过的模组的 setUpModule() 和 tearDownModule() 不会被运行。

跳过测试的基本用法如下:

import sys
import unittest

class TestStringMethods(unittest.TestCase):

    @unittest.skip("管你呢,不用测试")
    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    @unittest.skipIf(sys.platform == "win32", "平台的为win32, 我也不用测试")
    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    @unittest.skipUnless(sys.api_version == "1013", "api版本为1013,我才不测试")
    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

    @unittest.expectedFailure
    def test_fail(self):
        self.assertTrue(False)

if __name__ == '__main__':
    unittest.main()

输出:

xsss
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK (skipped=3, expected failures=1)

skipped:平台的为win32, 我也不用测试
skipped:api版本为1013,我才不测试
skipped:管你呢,不用测试

 Python: Mock 常用功能

Python 3.3 新增了一个可以用来在单元测试的时候进行 mock 操作的 unittest.mock 模块。 对于 Python 3.3 之前的版本也可以通过 pip install mock 安装拥有相同功能的移植版模块 mock 。本文记录一些常用的 unittest.mock 模块的用法,理论上同样也适用于 mock 这个第三方模块(注:本文例子是在 Python 3.6 下测试的)。

定义返回值

import example
import unittest
from unittest.mock import patch

# example.py:
# def test_func(x):
#     print("example" + str(x))
#     return x


class TestExample(unittest.TestCase):

    @patch('example.test_func')
    def test1(self, mock_func):
        mock_func.return_value = "打桩返回666"
        # mock_func.side_effect = ["打桩返回111", "打桩返回222", "打桩返回333"]
        a = example.test_func(1)
        print(a)
        b = example.test_func(2)
        print(b)
        c = example.test_func(3)
        print(c)
        mock_func.assert_called_with(3)


if __name__ == '__main__':
    unittest.main()

输出:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
打桩返回666
打桩返回666
打桩返回666

定义变化的返回值 

import example
import unittest
from unittest.mock import patch

# example.py:
# def test_func(x):
#     print("example" + str(x))
#     return x

class TestExample(unittest.TestCase):

    @patch('example.test_func')
    def test1(self, mock_func):
        # mock_func.return_value = "打桩返回666"
        mock_func.side_effect = ["打桩返回111", "打桩返回222", "打桩返回333"]
        a = example.test_func(1)
        print(a)
        b = example.test_func(2)
        print(b)
        c = example.test_func(3)
        print(c)
        mock_func.assert_called_with(3)


if __name__ == '__main__':
    unittest.main()

输出:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
打桩返回111
打桩返回222
打桩返回333

模拟对象中的方法、属性

下面这个类里面有一个属性 before ,一个方法 spawnu ,看下面的例子

class pexpect(object):
    """Fake pexpect class"""
    def __init__(self):
        """INIT"""
        self.before = None

    def spawnu(self):
        """Fake method"""
        pass


class UnitTest(unittest.TestCase):
    @mock.PropertyMock(pexpect, "before")
    @mock.patch.object(pexpect, "spawnu")
    def test_send_cli_cmd(self, mock_spawnu, mock_before):
        pass

使用 with 语法

上面介绍过适用于函数内部直接用 mock_func = mock.patch() 模拟的方式,也介绍过在函数或方法上用装饰器 @mock.patch() 的方式模拟。除此之外还可以用 with 语句模拟,比如下面几段代码的功能是相同的:

# 函数内部直接模拟
import os
from unittest import mock

def function():
    mock_func = mock.patch("os.path.isfile", return_value=True)


# 使用装饰器
@mock.patch("os.path.isfile")
def function(mock_os_isfile):
    mock_os_isfile.return_value = True


# 使用 with 语句
def function():
    with mock.patch("os.path.isfile") as mock_os_isfile:
        mock_os_isfile.return_value = True

如果同时模拟多个模块或方法,那么多个 mock 之间用斜杠分隔,就像这样:

def test_run(self):
    """UT Case"""
    with mock.patch.object(PolicyLookup, "compare_zone", return_value=None) as mock_compare_zone, \
         mock.patch.object(PolicyLookup, "write_data_to_database", return_value=None) as mock_w

检查调用

mock 的对象拥有一些可以用于单元测试的检查方法,可以用来测试 mock 对象的调用情况。

  • .called: 是否被调用过
>>> mock_func.called
True
>>>
  • .call_count: 获取调用次数
>>> mock_func.call_count
3
  • .assert_called(): 检查是否被调用过,如果没有被调用过,则会抛出 AssertionError 异常
>>> mock_func.assert_called()
>>>
  • .assert_called_once(): 确保调用过一次,如果没调用或多于一次,则抛出 AssertionError 异常
>>> mock_func.assert_called_once()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/xxx/lib/python3.6/unittest/mock.py", line 795, in assert_called_once
    raise AssertionError(msg)
AssertionError: Expected 'mock' to have been called once. Called 3 times.
>>>
  • .assert_not_called(): 确保没被调用过,否则抛出 AssertionError 异常
>>> mock_func.assert_not_called()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/xxx/lib/python3.6/unittest/mock.py", line 777, in assert_not_called
    raise AssertionError(msg)
AssertionError: Expected 'mock' to not have been called. Called 3 times.
  • call_args

返回 mock 的东西在调用时传入的具体参数

>>> mock_send_shell_cmd.some_method3(cmd="ls -l", mode="shell")
>>> mock_send_shell_cmd.some_method3.call_args
call(cmd="ls -l", mode="shell")

还有一个叫 call_args_list ,这个用于 mock 的方法被多次调用的情况,会返回一个列表,列表中是每次被调用时的参数


参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值