Python Language
Тестирование устройства
Поиск…
замечания
Для Python существует несколько модулей тестирования. В этом разделе документации описывается базовый модуль unittest
. Другие инструменты тестирования включают py.test
и nosetests
. В этой документации по тестированию python сравниваются некоторые из этих инструментов без углубления.
Исключения для тестирования
Программы выдают ошибки, если, например, задан неправильный ввод. Из-за этого необходимо убедиться, что при выдаче фактического неправильного ввода возникает ошибка. Из-за этого нам нужно проверить точное исключение, для этого примера мы воспользуемся следующим исключением:
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
для проверки этого исключения. assertRaises
можно использовать двумя способами:
- Использование обычного вызова функции. Первый аргумент принимает тип исключения, второй - вызываемый (обычно функция), а остальные аргументы передаются этому вызываемому.
- Использование предложения
with
, предоставляющее только тип исключения для этой функции. У этого есть преимущество, что больше кода может быть выполнено, но его следует использовать с осторожностью, поскольку несколько функций могут использовать одно и то же исключение, которое может быть проблематичным. Пример: с self.assertRaises (WrongInputException): convert2number ("not 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
.
Смысловые функции с 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 или нос:
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
в этих методах, чтобы также tearDown
методы 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
если 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)
Другим преимуществом регистрации очистки таким образом является то, что он позволяет программисту поместить код очистки рядом с установочным кодом, и он защитит вас в том случае, если субкласс забывает называть super
в tearDown
.
Утверждение об исключениях
Вы можете проверить, что функция генерирует исключение со встроенным 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)
Исключение для проверки должно быть первым параметром, а вызываемая функция должна быть передана как второй параметр. Любые другие указанные параметры будут переданы непосредственно вызываемой функции, что позволит вам указать параметры, которые инициируют исключение.
Выбор утверждений в рамках Unittests
В то время как у 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