Тестирование имеет решающее значение. Хотя существует много разных видов и уровней тестирования, хорошая библиотека поддерживает только для модульных тестов (пакет юнит-тестов Python и его моральные эквиваленты в других языках). Тем не менее, модульное тестирование не охватывает все виды тестирования, которые мы можем захотеть сделать, например, все виды целых программных тестов и интеграционных тестов. Именно здесь мы обычно получаем собственный скрипт «runner».
Написав свою долю таких пользовательских тестов, я недавно обратился к очень удобному подходу, которым хочу поделиться здесь. Короче говоря, я использую юнит-тесты Python в сочетании с динамической природой языка для запуска всех видов тестов.
Давайте предположим, что мои тесты представляют собой какие-то файлы данных, которые должны быть переданы в программу. Вывод программы сравнивается с некоторым файлом «ожидаемых результатов» или, возможно, каким-то образом кодируется в самом файле данных. Детали этого не имеют значения, но опытные программисты обычно очень часто сталкиваются с такими тестовыми установками. Обычно это происходит, когда тестируемая программа представляет собой механизм преобразования данных (компилятор, шифратор, кодировщик, компрессор, транслятор и т. Д.).
Итак, вы пишете «тестовый бегун». Скрипт, который просматривает какое-то дерево каталогов, находит там все «тестовые файлы», запускает каждый через преобразование, сравнивает, сообщает и т. Д. Я уверен, что все эти тестовые прогоны имеют общую инфраструктуру — я знаю, что мои ,
Почему бы не использовать существующие возможности Python для «тестирования бегуна», чтобы сделать то же самое?
Вот очень короткий фрагмент кода, который может служить шаблоном для достижения этой цели:
import unittest class TestsContainer(unittest.TestCase): longMessage = True def make_test_function(description, a, b): def test(self): self.assertEqual(a, b, description) return test if __name__ == '__main__': testsmap = { 'foo': [1, 1], 'bar': [1, 2], 'baz': [5, 5]} for name, params in testsmap.iteritems(): test_func = make_test_function(name, params[0], params[1]) setattr(TestsContainer, 'test_{0}'.format(name), test_func) unittest.main()
Что здесь происходит:
- Тестовый класс TestsContainer будет содержать динамически генерируемые тестовые методы.
- make_test_function создает тестовую функцию (точнее, метод), которая сравнивает ее входные данные. Это всего лишь тривиальный шаблон — он может делать что угодно, или таких «создателей» может быть несколько.
- Цикл создает тестовые функции из описания данных в testmap и присоединяет их к тестовому классу.
Имейте в виду, что это очень простой пример. Я надеюсь, что очевидно, что testmap действительно может быть тестовыми файлами, найденными на диске, или чем-то еще. Основная идея здесь — создание метода динамического тестирования.
Итак, что мы можем извлечь из этого, спросите вы? Достаточно много. unittest мощен — вооружен до зубов полезными инструментами для тестирования. Теперь вы можете вызывать тесты из командной строки, контролировать многословность, управлять поведением «быстрых сбоев», легко фильтровать, какие тесты запускать, а какие не запускать, использовать всевозможные методы утверждений для удобства чтения и составления отчетов (зачем писать собственное сравнение со смарт-списками) утверждения?). Более того, вы можете использовать любое количество сторонних инструментов для работы с результатами тестирования юнитов — отчеты HTML / XML, ведение журнала, автоматическая интеграция CI и так далее. Возможности безграничны.
Одна интересная вариация на эту тему — направить динамическое поколение на другой «слой» тестирования. unittest определяет любое количество «тестовых случаев» (классов), каждое из которых содержит любое количество «тестов» (методов). В приведенном выше коде мы генерируем несколько тестов в одном тестовом примере. Вот пример вызова, чтобы увидеть это в действии:
$ python dynamic_test_methods.py -v test_bar (__main__.TestsContainer) ... FAIL test_baz (__main__.TestsContainer) ... ok test_foo (__main__.TestsContainer) ... ok ====================================================================== FAIL: test_bar (__main__.TestsContainer) ---------------------------------------------------------------------- Traceback (most recent call last): File "dynamic_test_methods.py", line 8, in test self.assertEqual(a, b, description) AssertionError: 1 != 2 : bar ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED (failures=1)
Как вы можете видеть, все пары данных в тестовой карте преобразуются в однозначно названные методы тестирования в одном тестовом примере TestsContainer.
Очень легко, мы можем сократить это по-другому, сгенерировав целый контрольный пример для каждого элемента данных:
import unittest class DynamicClassBase(unittest.TestCase): longMessage = True def make_test_function(description, a, b): def test(self): self.assertEqual(a, b, description) return test if __name__ == '__main__': testsmap = { 'foo': [1, 1], 'bar': [1, 2], 'baz': [5, 5]} for name, params in testsmap.iteritems(): test_func = make_test_function(name, params[0], params[1]) klassname = 'Test_{0}'.format(name) globals()[klassname] = type(klassname, (DynamicClassBase,), {'test_gen_{0}'.format(name): test_func}) unittest.main()
Большая часть кода здесь остается прежней. Разница заключается в строках внутри цикла: теперь вместо динамического создания методов тестирования и их присоединения к тестовому сценарию мы создаем целые тестовые наборы — по одному на элемент данных с одним методом тестирования. Все тестовые случаи происходят от DynamicClassBase и, следовательно, от unittest.TestCase, поэтому они будут автоматически обнаружены механизмом unittest. Теперь выполнение будет выглядеть так:
$ python dynamic_test_classes.py -v test_gen_bar (__main__.Test_bar) ... FAIL test_gen_baz (__main__.Test_baz) ... ok test_gen_foo (__main__.Test_foo) ... ok ====================================================================== FAIL: test_gen_bar (__main__.Test_bar) ---------------------------------------------------------------------- Traceback (most recent call last): File "dynamic_test_classes.py", line 8, in test self.assertEqual(a, b, description) AssertionError: 1 != 2 : bar ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1)
Почему вы хотите динамически генерировать целые тестовые случаи, а не отдельные тесты? На самом деле все зависит от ваших конкретных потребностей. В целом, тестовые случаи лучше изолированы и делятся меньше, чем тесты в одном тестовом примере. Более того, у вас может быть огромное количество тестов, и вы хотите использовать инструменты, которые ограждают ваши тесты для параллельного выполнения — в этом случае вам почти наверняка потребуются отдельные тестовые случаи.
За последние пару лет я использовал эту технику во многих проектах и нашел ее очень полезной; я неоднократно заменял целую сложную программу для выполнения тестов примерно 20-30 строками кода, используя эту технику, и получал доступ ко многим другим возможностям бесплатно.
Встроенные в Python средства обнаружения, создания отчетов и запуска очень мощные. В сочетании со сторонними инструментами они могут быть еще более мощными. Использование всего этого потенциала для любого вида тестирования, а не только для модульного тестирования, возможно с очень небольшим количеством кода, благодаря динамичности Python. Я надеюсь, вы тоже найдете это полезным.