Pull to refresh

Тестирование. Начало

Python *

Привет. В этой серии постов я попробую рассказать про тестирование кода на питоне, в частности проектов 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 и как с ними бороться.
Tags: pythonunittestdoctest
Hubs: Python
Total votes 130: ↑119 and ↓11 +108
Comments 15
Comments Comments 15

Popular right now