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


    Привет. В этой серии постов я попробую рассказать про тестирование кода на питоне, в частности проектов 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 и как с ними бороться.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 15
    • –11
      Интересно, какой ***звон поставил топику минус?
      Я вот пару нюансов для себя нашел, спасибо автору!
      • 0
        Судя по всему тот же который поставил минус тебе
        • 0
          Пора вводить плюсовую валютную систему:

          1. Каждый плюс и каждый минус дают 0. Таким образом надо заработать плюсы, что бы ставить минусы. А так же иметь возможность купить скажем минусы за $1 и использовать это как валюту. Таким образом выражение негативных эмоций будет сдерживаться здравым смыслом. Тот кто тяжело заработал плюс будет тяжело с ним расставаться.

          2. Каждый голос можно снабдить строчным комментарием, а так же ввести апелляцию по голосованию.

          P.S. Искал материал по написанию unittest и нашел статью. Для меня статья была полезна.

      • 0
        Спасибо за статью.

        p.s. I'm test infected.
        • НЛО прилетело и опубликовало эту надпись здесь
          • +1
            nose как-то бегло освещен. Тем, кто не сталкивался, nose автоматически собирает тесты из файлов начинающихся на test_, он достаточно умен чтобы заглянуть в папочку tests, если такая присутствует, может показать покрытие кода тестами (модуль coverage). Имеет удобный режим, когда запускаются только не прошедшие в прошлый прогон тесты.
            • 0
              Спасибо большое, добавил в пост.
              • 0
                Более того, тесты не обязательно должны наследоваться от unittest.TestCase, он соберет классы начинающиеся на определенное регулярное выражение, тут описано подробнее: somethingaboutorange.com/mrl/projects/nose/1.0.0/finding_tests.html

                И в примере звезда '*' не нужна скорее всего, будет работать и без нее.
              • НЛО прилетело и опубликовало эту надпись здесь
                • 0
                  Классная штука, не знал о ней, спасибо. Похожий синтаксис с явной регистрацией и декораторами предлагали для unittest2, но не реализовали, хотя мне это тоже кажется отличной идеей.
                • 0
                  Спасибо за статью, в которой рассказано про множество методов тестирования. Теперь на вопрос в жаббере «Расскажи как тестить код в python?» можно просто кинуть одну ссылку.
                  • +2
                    Несмотря на всю ограниченость doctest-ов их можно вполне удачно использовать когда серьезные тестовые библиотеки кажуться overkill-ом:

                    def test(f):
                        f.__doc__ = '>>> %s()' % f.func_name
                        return f

                    @test
                    def foo():
                        assert "b" + "a" + "z" == "baz"

                    if __name__ == '__main__':
                        import doctest
                        doctest.testmod()
                    • 0
                      www.lyabah.com/index.php/2011/06/10/testing-django-fixtures/

                      Только недавно писал статью у себя на тему джанго-тестов и фикстур.

                      Возможно автору будет полезно для следующих статей
                      • 0
                        Скажите, а для анализа coverage тестами в ключе питоновского кода что порекомендуете использовать?

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.