Тестирование исключений
Программы выдают ошибки, когда, например, вводится неправильный ввод. Из-за этого нужно удостовериться, что выдается ошибка, когда вводится неправильный ввод. Из-за этого нам нужно проверить точное исключение, для этого примера мы будем использовать следующее исключение:
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
пунктом, давая только тип исключения для функции. Преимущество этого заключается в том, что можно выполнять больше кода, но его следует использовать с осторожностью, поскольку несколько функций могут использовать одно и то же исключение, которое может быть проблематичным. Пример: с self.assertRaises (WrongInputException):
Сначала это было реализовано в следующем тестовом примере:
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
методы.
Перемешивание функций с помощью unittest.mock.create_autospec
Один из способов для имитации функции заключается в использовании create_autospec
функции, которая будет макет из объекта в соответствии с его характеристиками. С помощью функций мы можем использовать это, чтобы гарантировать, что они вызываются соответствующим образом.
С функцией multiply
в custom_math.py
:
def multiply(a, b):
return a * b
А функция multiples_of
в process_math.py
:
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):
"""
Passing in args and kwargs here will only raise TypeError if values were
passed to multiples_of function, otherwise they are ignored. This way we can
test that multiples_of is used correctly. This is here for an illustration
of how create_autospec works. Not recommended for production code.
"""
multiple = multiply(integer,x, *args, **kwargs)
multiples.append(multiple)
return multiples
Мы можем проверить multiples_of
в одиночку, насмехаясь над из multiply
.В приведенном ниже примере используется стандартная библиотека Python unittest, но это можно использовать и с другими средами тестирования, такими как pytest или nose:
from unittest.mock import create_autospec
import unittest
# we import the entire module so we can mock out 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) # this should raise a TypeError
Тестовая настройка и разрушение в пределах unittest.TestCase
Иногда мы хотим подготовить контекст для каждого запускаемого теста. setUp
метод запускается перед каждым испытанием в классе. tearDown
запускается в конце каждого теста. Эти методы не являются обязательными. Помните , что TestCases часто используется в кооперативном множественном наследовании , так что вы должны быть осторожны , чтобы всегда вызывать 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()
Обратите внимание , что в python2.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)
Еще одно преимущество регистрации ыборкы таким образом, что она позволяет программисту поставить очищающий код рядом с кодом установки и защищает вас в том случае, если subclasser забывает назвать super
в tearDown
.
Утверждение об исключениях
Вы можете проверить, что функция генерирует исключение с помощью встроенного юнит-теста двумя разными способами.
Использование менеджера контекста
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)
Исключением для проверки должен быть первый параметр, а вызываемая функция должна быть передана как второй параметр. Любые другие указанные параметры будут переданы непосредственно в вызываемую функцию, что позволит вам указать параметры, которые вызывают исключение.
Выбор утверждений в рамках юнит-тестов
В то время как 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
.Еще раз, использование отрицательных аналогов при необходимости приведет к более четким сообщениям об ошибках.
Юнит тесты с pytest
установка pytest:
pip install pytest
подготовка тестов:
mkdir tests
touch tests/test_docker.py
Функции для тестирования в docker_something/helpers.py
:
from subprocess import Popen, PIPE
# this Popen is monkeypatched with the 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():
'''Used to collect bytes
'''
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
Обезьяна заплат с pytest в test_docker.py
:
@pytest.fixture
def all_popens(monkeypatch):
'''This fixture overrides / mocks the builtin Popen
and replaces stdin, stdout, stderr with a MockBytes object
note: monkeypatch is magically imported
'''
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_
в test_docker.py
файле:
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