Test-Driven Development в Python для начинающих, часть вторая

Alexander Shepetko28 октября, 11:00 81 0

Базовые концепции TDD и примеры на Python с использованием пакета nose.

В предыдущей части статьи мы рассмотрели использование пакета nosetest для организации юнит-тестирования. Сегодня мы поговорим о некоторых других средствах тестирования и отладки Python-программ.

Другие пакеты для юнит-тестирования

py.test

Это похожий на nosetest инструмент для запуска тестов, который использует те же самые соглашения, то есть, вы сможете запускать тесты, подготовленные для nosetest также и в pytest. Интересной возможностью pytest является захват stdout и отображение его отдельно после выполнения теста. Автор находит pytest более удобным для запуска отдельных тестов, а не серий из них.

Для того, чтобы установить py.test, вам понадобится старый-добрый pip. Просто выполните pip install pytest и вы получите свежую версию со всеми установленными зависимостями. Вы можете запускать серии тестов, находящихся в каталоге, указав путь к нему или же выполнить отдельный тест, соответственно передав путь к файлу теста.

$ py.test test/test_calculator.py
================================ test session starts ================================
platform darwin -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
collected 4 items 
 
test/test_calculator.py ....
 
================================ 4 passed in 0.02 seconds ================================

Ниже показан пример работы pytest с выводом захваченного stdout. Это часто может быть полезно при отладке и поиске ошибок. Обратите внимание, что pytest выводит это только в случае, если тест завершился неудачей. В противном случае вы увидите только стандартные сообщения.

$ py.test test/test_calculator.py 
================================ test session starts ================================
platform darwin -- Python 2.7.6 -- py-1.4.26 -- pytest-2.6.4
collected 4 items 
 
test/test_calculator.py F...
 
============================= FAILURES =============================
________________________________________ TddInPythonExample.test_calculator_add_method_returns_correct_result _________________________________________
 
self = 
 
    def test_calculator_add_method_returns_correct_result(self):
        result = self.calc.add(3, 2)
>       self.assertEqual(4, result)
E       AssertionError: 4 != 5
 
test/test_calculator.py:11: AssertionError
------------------------------------ Captured stdout call ------------------------------------
X value is: 3
Y value is: 2
Result is 5
============================= 1 failed, 3 passed in 0.03 seconds =============================

UnitTest

Поставляемый вместе с Python пакет unittest, которым мы пользовались, когда создавали наши тесты, кроме всего прочего умеет и запускать тесты, предоставляя красивый вывод результатов. Использование unittest может быть очень кстати, если вы не хотите вносить дополнительные зависимости в ваше приложение, используя лишь возможности стандартной библиотеки. Просто добавьте следующий код в конец вашего тест-файла:

if __name__ == '__main__':
    unittest.main()

И запустите тест:

$ python test/test_calculator.py 
....
----------------------------------------------------------------------
Ran 4 tests in 0.004s
 
OK

Отладка кода

Часто, при использовании TDD-подхода, вы будете сталкиваться с ситуациями, когда тесты будут завершаться с ошибкой. Также, вдобавок к этому, обязательно будут случаи, когда вы сходу не можете определить причину ошибки. В подобных случаях вам необходимо будет применять какие-то методы отладки кода, чтобы понять, какими данными и в какой момент оперирует код, приводящий к ошибке. Новички, когда только начинают работать с Python, часто используют print(), выводя в нужных точках значения переменных прямо на консоль, чтобы понять, что происходит.

Отладка с использованием print()

Чтобы понять, как используется print() для отладки, давайте изменим наш калькулятор так, чтоб один из его методов работал неправильно. Возьмём метод add() и сделаем так, чтобы он не складывал числа, а вычитал:

class Calculator(object):
    def add(self, x, y):
        number_types = (int, long, float, complex)
 
        if isinstance(x, number_types) and isinstance(y, number_types):
            return x - y
        else:
            raise ValueError

Теперь, если запустить соответствующий тест, проверяющий, что add() возвращает результат сложения двух чисел, то тест завершится с ошибкой. Допустим теперь, что вы не знаете в чём причина появившейся ошибки и хотите проверить, какие же на самом деле значения add() получает на входе и что возвращает. Вы просто добавляете print() там, где это нужно:

class Calculator(object):
    def add(self, x, y):
        number_types = (int, long, float, complex)
 
        if isinstance(x, number_types) and isinstance(y, number_types):
            print 'X is: {}'.format(x)
            print 'Y is: {}'.format(y)
            result = x - y
            print 'Result is: {}'.format(result)
            return result
        else:
            raise ValueError

Теперь, когда вы запустите nosetest, вы получите вывод ваших print(), что даст возможность отыскать причину ошибки:

$ nosetests test/test_calculator.py
F...
======================================================================
FAIL: test_calculator_add_method_returns_correct_result (test.test_calculator.TddInPythonExample)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/user/PycharmProjects/tdd_in_python/test/test_calculator.py", line 11, in test_calculator_add_method_returns_correct_result
    self.assertEqual(4, result)
AssertionError: 4 != 0
-------------------- >> begin captured stdout << ---------------------
X is: 2
Y is: 2
Result is: 0
 
--------------------- >> end captured stdout << ----------------------
 
----------------------------------------------------------------------
Ran 4 tests in 0.002s
 
FAILED (failures=1)

Отладка при помощи PDB

Как только вы начнёте создавать мало-мальски сложный код, вы вероятней всего придёте к выводу, что использование print() -- это малость неудобно, и будете правы. Ведь мало того, что эти print() нужно везде расставить, их потом ещё нужно не забыть отовсюду убрать. Таким образом разработчики пришли к выводу, что нужны какие-то более удобные инструменты, одним из которых стал широко используемый pdb, также известный как Python Debugger, входящий в набор стандартной библиотеки Python.

Чтобы воспользоваться pdb, вам необходимо вставить в нужном месте небольшой кусочек кода, называемый breakpoint, в нужном месте. Дойдя до этого места, выполнение сценария будет приостановлено и запустится Python Debugger, который даст вам возможность в интерактивном режиме выполнить отладку кода. Давайте поэкспериментируем над нашим "неправильным" методом:

class Calculator(object):
    def add(self, x, y):
        number_types = (int, long, float, complex)
 
        if isinstance(x, number_types) and isinstance(y, number_types):
            import pdb; pdb.set_trace()
            return x - y
        else:
            raise ValueError

Если вы используете nosetest для запуска теста, не забудьте добавить флаг -s для того, чтобы отключить захват вывода, иначе вы не увидите приглашения pdb и не сможете воспользоваться интерактивным режимом. В случае, если вы используете традиционный unittest или pytest, то никаких дополнительных флагов не требуется.

Итак, после того, как вы запустите тест, он будет приостановлен в том месте, где вызывается pdb.set_trace(), после чего вы получите возможность взаимодействовать с кодом и переменными, доступными с текущей области видимости. Если теперь в приглашении pdb выполнить команду list, вы сможете увидеть текущее местоположение в коде:

$ nosetests -s
> /Users/user/PycharmProjects/tdd_in_python/app/calculator.py(7)add()
-> return x - y
(Pdb) list
  2          def add(self, x, y):
  3             number_types = (int, long, float, complex)
  4    
  5             if isinstance(x, number_types) and isinstance(y, number_types):
  6                 import pdb; pdb.set_trace()
  7  ->              return x - y
  8             else:
  9                 raise ValueError
[EOF]
(Pdb)

Вы можете взаимодействовать с кодом так, будто бы вы находитесь в оболочке интерпретатора Python. Например, можно посмотреть, какие значения хранят переменные x и y:

(Pdb) x
2
(Pdb) y
2

Подобным образом вы можете играть с кодом так, как вам необходимо, пытаясь найти источник проблемы. В любом месте вы можете воспользоваться командой help, чтобы получить список доступных команд, среди которых можно выделить наиболее часто используемые:

  • n: перейти на следующую строку выполнения кода;
  • list: получить текущее местоположение в коде;
  • args: получить список переменных, доступных в текущей области видимости;
  • continue: продолжить выполнение кода без остановок;
  • jump <номер строки>: продолжить выполнение кода, пока не будет достигнута определённая строка;
  • quit / exit: завершить работу pdb.

Заключение

Разработка через тестирование -- это захватывающий процесс, который поможет вам значительно улучшить качество кода. Эту методику можно применять одинаково успешно как в небольших проектах, так и при разработке сложных систем, в разработке которых участвует большое количество разработчиков. Неважно, практикуете ли вы парное программирование или занимаетесь написанием кода самостоятельно, написание кода, который постепенно проходит всё большее и большее количество тестов без ошибок, приносит массу удовольствия. Если вы относитесь к тем разработчикам, которые считают, что разработка тестов -- это пустая трата времени, то автор очень надеется, что эта статья заставит вас хоть немного пересмотреть ваши взгляды.