
Привет. В этой серии постов я попробую рассказать про тестирование кода на питоне, в частности проектов django. Мы рассмотрим модульное тестирование (юнит-тесты), статический анализ кода и некоторые подводные камни тестирования веб-сайтов.
Вводную часть о пользе тестирования опустим — код, покрытый тестами, становится мягким и шелковистым, про это только ленивый еще не читал / писал.
unittest
Стандартный модуль для реализации юнит-тестов (unittest, ранее pyunit) появился в питоне версии 2.1 и являл собой порт JUnit с Java (даже именование методов оставили camelCase, вопреки pep8). В python 2.7 (3.2) в unittest добавили много новых интересных вещей: дополнительные проверки (
assert*), декораторы, позволяющие пропустить отдельный тест (@skip, @skipIf) или обозначить сломанные тесты, о которых разработчику известно (@expectedFailure), изменился способ запуска из командной строки. Существует также порт этих изменений для питона 2.4 и выше, называется unittest2.Как эта штука работает. Предположим, у нас есть нечеловечески сложный модуль, run_once.py:
def run_once(f): """ Мемоизация. Не зависит от аргументов. """ def _f(*args, **kwargs): if not hasattr(_f, "_retval"): _f._retval = f(*args, **kwargs) return _f._retval return _f
(Это декоратор, который сохраняет результат первого вызова функции-параметра и всегда возвращает сохраненное значение.)
Тест нашего модуля может выглядеть как-то так:
import unittest class Test(unittest.TestCase): def test_run_once(self): @run_once def inc(n): return n + 1 # это результат вызова функции inc()... self.assertEqual(inc(7), 8) # ...а это -- сохраненное значение self.assertEqual(inc(0), 8) if __name__ == "__main__": unittest.main()
Последние две строки представляют собой пускалку, которая найдет и запустит все тесты в этом модуле. Теперь в консоли мы можем выполнить
$ python run_once.py
И пронаблюдать результат тестирования:
. --- Ran 1 test in 0.000s OK
doctest
Поскольку юнит-тестами в наши дни никого не удивишь, покажу одну штуку, специфичную для питона — доктесты (doctests). Это действительно проще показать, чем объяснить:
def run_once(f): """ >>> @run_once ... def foo(n): return n + 1 >>> foo(7) 8 >>> foo(0) 8 """ def _f(*args, **kwargs): if not hasattr(_f, "_retval"): _f._retval = f(*args, **kwargs) return _f._retval return _f if __name__ == "__main__": import doctest doctest.testmod()
Ключ на старт:
$ python run_once2.py -v Trying: @run_once def foo(n): return n + 1 Expecting nothing ok Trying: foo(7) Expecting: 8 ok Trying: foo(0) Expecting: 8 ok 1 items had no tests: __main__ 1 items passed all tests: 3 tests in __main__.run_once 3 tests in 2 items. 3 passed and 0 failed. Test passed.
Мы видим, что docstring функции превратился в пример кода, одинаково понятный (надеюсь) и разработчику, и интерпретатору.
По сравнению с классическими юнит-тестами, у доктестов есть как плюсы (простота написания, можно скопировать прямо из интерактивной сессии питона; документация всегда соответствует коду), так и минусы (сложный код быстро становится нечитаемым; текстовый редактор не подсветит такой код, а статический анализатор не найдет в нем ошибок). Впрочем, ничто не мешает применять докстесты для мелких очевидных вещей (как в примере), и юнит-тесты для более сложных задач.
py.test
Наряду с входящими в стандартную поставку питона средствами, существуют и альтернативные инструменты, например, py.test. Инсталляция происходит как обычно,
easy_install -U pytest # или pip install -U pytest
Возьмем функцию из первого примера. Видоизмененный юнит-тест будет выглядеть как-то так:
def test_run_once(): @run_once def inc(n): return n + 1 # это результат вызова функции inc()... assert inc(7) == 8 # ...а это -- сохраненное значение assert inc(0) == 8
Поехали:
$ py.test run_once3.py === test session starts === platform darwin -- Python 2.6.1 -- pytest-2.0.3 collected 1 items run_once3.py . === 1 passed in 0.01 seconds ===
Ключевые особенности py.test (хорошие): никакого API (справедливости ради: в исключительных случаях API все же бывает нужно, но его очень мало); проверки посредством assert. Это обеспечивает потенциальную возможность запустить тест даже без установленного py.test, например, на продакшен-сервере после выкладки (мало ли). Тесты можно оформлять как классами (в стиле unittest), так и просто функциями вида
test_*.У отсутствия API, впрочем, есть и обратная сторона: новый разработчик, подключившись к проекту, может просто не понять, как запустить этот ворох функций. Впрочем, настолько новым разработчик пробудет совсем недолго, а способ запуска тестов лучше в любом случае документировать, от греха подальше.
nose
nose — это инструмент для прогона тестов посредством unittest (и doctest, с ключом
--with-doctest). Имеет также собственное API, использовать которое необязательно. Успешно отрабатывает на всех приведенных выше примерах:$ nosetests * -v --with-doctest test_run_once (run_once.Test) ... ok Doctest: run_once2.run_once ... ok run_once3.test_run_once ... ok --- Ran 3 tests in 0.008s OK
Как подсказывает Yur4eg (спасибо!), nose автоматически собирает тесты из файлов вида
test_*, достаточно умен, чтобы заглянуть в папочку tests при наличии таковой, умеет измерять покрытие кода (code coverage) при помощи coverage.py (--with-coverage--failedЗа сим откланиваюсь. Осталось только приложить исходники примеров, три штуки. Public domain.
В следующем выпуске: штатные средства тестирования django и как с ними бороться.