Section 192.1: 在 unittest.TestCase 中的测试设置和清理
有时我们希望为每个测试准备一个上下文。setUp 方法会在类中的每个测试之前运行。tearDown 方法会在每个测试结束时运行。这些方法是可选的。请记住,TestCase 常用于协作式多重继承,因此您应该始终在这些方法中调用 super,以确保基类的 setUp 和 tearDown 方法也能被调用。TestCase 的基实现提供了空的 setUp 和 tearDown 方法,以便可以调用它们而不会引发异常:
import unittest
class SomeTest(unittest.TestCase):
def setUp(self):
super(SomeTest, self).setUp()
self.mock_data = [1,2,3,4,5]
def test(self):
self.assertEqual(len(self.mock_data), 5)
def tearDown(self):
super(SomeTest, self).tearDown()
self.mock_data = []
if __name__ == '__main__':
unittest.main()
在 Python 2.7+ 中,还有 addCleanup 方法,它注册了在测试运行后要调用的函数。与 tearDown(只有在 setUp 成功时才会被调用)不同,通过 addCleanup 注册的函数即使在 setUp 中发生未处理的异常也会被调用。例如,此方法通常用于移除在测试运行期间注册的各种模拟对象:
import unittest
import some_module
class SomeOtherTest(unittest.TestCase):
def setUp(self):
super(SomeOtherTest, self).setUp()
# Replace `some_module.method` with a `mock.Mock`
my_patch = mock.patch.object(some_module, 'method')
my_patch.start()
# When the test finishes running, put the original method back.
self.addCleanup(my_patch.stop)
通过这种方式注册清理函数的另一个好处是,它允许程序员将清理代码放在设置代码旁边,并且在子类继承者忘记在 tearDown 中调用 super 时,它也能保护您。
Section 192.2: 断言异常
您可以通过内置的 unittest 通过两种不同的方法来测试函数是否抛出异常。
使用上下文管理器
def division_function(dividend, divisor):
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError):
x = division_function(1, 0)
这将运行上下文管理器中的代码,并且,如果它成功运行,测试将失败,因为没有抛出异常。如果代码抛出了正确类型的异常,测试将继续。
您还可以获取抛出异常的内容,如果需要对它执行额外的断言。
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError) as ex:
x = division_function(1, 0)
self.assertEqual(ex.message, 'integer division or modulo by zero')
通过提供可调用函数
def division_function(dividend, divisor):
"""
Dividing two numbers.
:type dividend: int
:type divisor: int
:raises: ZeroDivisionError if divisor is zero (0).
:rtype: int
"""
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_passing_function(self):
self.assertRaises(ZeroDivisionError, division_function, 1, 0)
要检查的异常必须是第一个参数,并且必须将可调用函数作为第二个参数传递。指定的任何其他参数将直接传递给被调用的函数,允许您指定触发异常的参数。
Section 192.3: 测试异常
当输入错误时,程序会抛出错误。因此,需要确保在实际输入错误时抛出错误。为此,我们需要检查确切的异常。在这个例子中,我们将使用以下异常:
class WrongInputException(Exception):
pass
当输入错误时,会抛出此异常。在以下上下文中,我们总是期望输入的是数字。
def convert2number(random_input):
try:
my_input = int(random_input)
except ValueError:
raise WrongInputException("Expected an integer!")
return my_input
要检查是否抛出了异常,我们使用 assertRaises 来检查该异常。assertRaises 可以以两种方式使用:
- 使用常规函数调用。第一个参数是异常类型,第二个参数是一个可调用对象(通常是函数),其余参数将传递给该可调用对象。
- 使用
with子句,只向函数提供异常类型。这有一个优点,可以执行更多代码,但应谨慎使用,因为多个函数可能使用相同的异常,这可能会有问题。例如:
with self.assertRaises(WrongInputException):
convert2number("not a number")
以下测试用例中实现了第一种方法:
import unittest
class ExceptionTestCase(unittest.TestCase):
def test_wrong_input_string(self):
self.assertRaises(WrongInputException, convert2number, "not a number")
def test_correct_input(self):
try:
result = convert2number("56")
self.assertIsInstance(result, int)
except WrongInputException:
self.fail()
可能还需要检查不应抛出的异常。然而,当抛出异常时,测试会自动失败,因此可能根本不需要。为了展示选项,第二个测试方法展示了如何检查不应抛出的异常。基本上,这是捕获异常并使用 fail 方法使测试失败。
Section 192.4: 在单元测试中选择断言
虽然 Python 有一个 assert 语句,但 Python 单元测试框架提供了更适合测试的断言:它们在失败时更具信息性,并且不依赖于执行的调试模式。
最简单的断言可能是 assertTrue,可以这样使用:
import unittest
class SimplisticTest(unittest.TestCase):
def test_basic(self):
self.assertTrue(1 + 1 == 2)
这将正常运行,但如果将上述行替换为
self.assertTrue(1 + 1 == 3)
将失败。
assertTrue 断言可能是最通用的断言,因为任何测试都可以被表示为某种布尔条件,但通常有更好的选择。当测试相等性时,如上所示,最好写为
self.assertEqual(1 + 1, 3)
当前者失败时,消息是
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertTrue(1 + 1 == 3)
AssertionError: False is not true
当前者失败时,消息是
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
更具信息性(它实际上评估了左侧的结果)。
您可以在标准文档中找到断言的列表。一般来说,选择最适合条件的断言是个好主意。因此,如上所示,对于断言 1 + 1 == 2,最好使用 assertEqual 而不是 assertTrue。同样,对于断言 a is None,最好使用 assertIsNone 而不是 assertEqual。
请注意,断言也有否定形式。因此,assertEqual 有其否定形式 assertNotEqual,assertIsNone 有其否定形式 assertIsNotNone。再次强调,适当使用否定形式将导致更清晰的错误消息。
Section 192.5: 使用 pytest 的单元测试
安装 pytest:
pip install pytest
准备测试:
mkdir tests
touch tests/test_docker.py
在 docker_something/helpers.py 中要测试的函数:
from subprocess import Popen, PIPE
# 这个 Popen 被用 fixture `all_popens` 替换了
def copy_file_to_docker(src, dest):
try:
result = Popen(['docker', 'cp', src, 'something_cont:{}'.format(dest)],
stdout=PIPE, stderr=PIPE)
err = result.stderr.read()
if err:
raise Exception(err)
except Exception as e:
print(e)
return result
def docker_exec_something(something_file_string):
fl = Popen(["docker", "exec", "-i", "something_cont", "something"],
stdin=PIPE, stdout=PIPE, stderr=PIPE)
fl.stdin.write(something_file_string)
fl.stdin.close()
err = fl.stderr.read()
fl.stderr.close()
if err:
print(err)
exit()
result = fl.stdout.read()
print(result)
在 test_docker.py 中导入测试:
import os
from tempfile import NamedTemporaryFile
import pytest
from subprocess import Popen, PIPE
from docker_something import helpers
copy_file_to_docker = helpers.copy_file_to_docker
docker_exec_something = helpers.docker_exec_something
在 test_docker.py 中模拟文件对象:
class MockBytes():
'''用于收集字节
'''
all_read = []
all_write = []
all_close = []
def read(self, *args, **kwargs):
# print('read', args, kwargs, dir(self))
self.all_read.append((self, args, kwargs))
def write(self, *args, **kwargs):
# print('wrote', args, kwargs)
self.all_write.append((self, args, kwargs))
def close(self, *args, **kwargs):
# print('closed', self, args, kwargs)
self.all_close.append((self, args, kwargs))
def get_all_mock_bytes(self):
return self.all_read, self.all_write, self.all_close
在 test_docker.py 中使用 pytest 进行猴子补丁:
@pytest.fixture
def all_popens(monkeypatch):
'''这个 fixture 替换 / 模拟了内置的 Popen
并将 stdin、stdout、stderr 替换为 MockBytes 对象
注意:monkeypatch 是神奇地导入的
'''
all_popens = []
class MockPopen(object):
def __init__(self, args, stdout=None, stdin=None, stderr=None):
all_popens.append(self)
self.args = args
self.byte_collection = MockBytes()
self.stdin = self.byte_collection
self.stdout = self.byte_collection
self.stderr = self.byte_collection
pass
monkeypatch.setattr(helpers, 'Popen', MockPopen)
return all_popens
在 test_docker.py 文件中以 test_ 为前缀的示例测试:
def test_docker_install():
p = Popen(['which', 'docker'], stdout=PIPE, stderr=PIPE)
result = p.stdout.read()
assert 'bin/docker' in result
def test_copy_file_to_docker(all_popens):
result = copy_file_to_docker('asdf', 'asdf')
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert mock_read
assert result.args == ['docker', 'cp', 'asdf', 'something_cont:asdf']
def test_docker_exec_something(all_popens):
docker_exec_something(something_file_string)
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert len(mock_read) == 3
something_template_stdin = mock_write[0][1][0]
these = [os.environ['USER'], os.environ['password_prod'], 'table_name_here', 'test_vdm',
'col_a', 'col_b', '/tmp/test.tsv']
assert all([x in something_template_stdin for x in these])
逐个运行测试:
py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests
运行 tests 文件夹中的所有测试:
py.test -k test_ tests
Section 192.6: 使用 unittest.mock.create_autospec 模拟函数
模拟函数的一种方法是使用 create_autospec 函数,它将根据对象的规格来模拟对象。对于函数,我们可以使用它来确保它们被正确调用。
在 custom_math.py 中有一个函数 multiply:
def multiply(a, b):
return a * b
在 process_math.py 中有一个函数 multiples_of:
from custom_math import multiply
def multiples_of(integer, *args, num_multiples=0, **kwargs):
"""
:rtype: list
"""
multiples = []
for x in range(1, num_multiples + 1):
"""
在这里传递 args 和 kwargs,如果向 multiples_of 函数传递了值,只会引发 TypeError,
否则它们将被忽略。这样我们可以测试 multiples_of 是否被正确使用。这里是为了说明
create_autospec 的工作方式,不推荐用于生产代码。
"""
multiple = multiply(integer, x, *args, **kwargs)
multiples.append(multiple)
return multiples
我们可以通过模拟 multiply 来单独测试 multiples_of。以下示例使用了 Python 标准库 unittest,但它也可以与其他测试框架一起使用,例如 pytest 或 nose:
from unittest.mock import create_autospec
import unittest
# 我们导入整个模块,以便可以模拟 multiply
import custom_math
custom_math.multiply = create_autospec(custom_math.multiply)
from process_math import multiples_of
class TestCustomMath(unittest.TestCase):
def test_multiples_of(self):
multiples = multiples_of(3, num_multiples=1)
custom_math.multiply.assert_called_with(3, 1)
def test_multiples_of_with_bad_inputs(self):
with self.assertRaises(TypeError) as e:
multiples_of(1, "extra arg", num_multiples=1) # 这应该引发一个 TypeError