naspeh :: Python. Компоновка и лаконичность тестов 15.01.2011
Код в тестах обычно простой, т.к. выполняет довольно тривиальные операции проверки, сравнения и т.д. Когда много похожего кода, то логично подумать о его краткости.
Note
nose у меня давно вошел в набор обязательных инструментов для тестирования, так что в примерах есть его влияние.
Подход 1. Класс-контейнер с тестами в виде методов, аля unittest
from unittest import TestCase
answer = 42
class TestAnswer(TestCase):
def setUp(self):
print('setup')
def test(self):
self.assertEquals(answer, 42)
def tearDown(self):
print('teardown')
Исторически сложилось, что unittest использует верблюжью нотацию для setUp, tearDown, assert* (assertTrue, assertEquals...) методов. Но в python есть PEP 8, в котором принято использовать подчеркивание в названиях функций (методов), и в nose.tools можно найти аналогичные функции, но с подчеркиванием (assert_true, assert_equals) для любителей PEP 8.
Подход 2. Модуль с тестами в виде функций
from nose.tools import assert_equal, with_setup
answer = 42
def setup_func():
print('setup')
def teardown_func():
print('teardown')
@with_setup(setup_func, teardown_func)
def test_answer():
assert_equal(answer, 42)
В последнее время второй подход компоновки тестов мне все больше нравится. Почему?
и в первом и во втором подходе приходится давать имя модулю, который будет содержать наши тесты. Но в первом нужно еще придумывать имя классу-контейнеру, а т.к. мне нравятся небольшие модули (в них проще ориентироваться), то в большинстве случаев название класса - тавтология
test_auth.py:TestAuth.test_login test_auth.py.test_login
в первом подходе вроде лучше выглядят SetUp, TearDown методы, во втором приходится импортировать декоратор with_setup. Но и тут можно выделить плюс, обычно название класса подбираю по содержимым тестам
class TestAuth(TestCase): def test_login() ... def test_logout() ...
но когда для test_login нужен setUp метод, а для test_logout нет, то тут приходится класс-контейнеры компоновать в зависимости от используемых SetUp, TearDown методов. В общем присутствует неоднозначность и это не очень хорошо :)
в классе-контейнере забирается один отступ, а отступы ценны, когда соблюдаешь ограничение в 80 символов;
в первом подходе класс наследуется от unittest.TestCase, при вызове каждого assert* метода логично обращаться к self и тут опять у нас крадут символы
self.assertEqual assert_equal...4
Для написания тестов можно использовать doctest
1 2 3 4 5 6 7 8 9 | answer = 42
def test_answer():
'''
>>> answer
42
'''
assert False
|
Выглядит кратко, хотя конечно такой формат тестов не всегда подходит...
Note
Если запускать через nose ($ nosetests --with-doctest), то строка 9 не вызывается.
Классная вещь assert
answer = 43
def test_answer():
assert answer == 42
После запуска, вывод:
$ nosetest
======================================================================
FAIL: test.test_answer
----------------------------------------------------------------------
Traceback (most recent call last):
...
assert answer == 42
AssertionError
Очень заманчиво: не нужен дополнительный импорт, лаконично. Но вот при выводе не известно какое значение содержит переменная answer. Правда тут может порадовать nose и даже двумя вариантами:
$ nosetests --pdb-failures
...
-> assert answer == 42
(Pdb) answer
43
приходится вводить answer - лишние телодвижения :)
Следующий вариант еще красивее:
$ nosetest -d
======================================================================
FAIL: test.test_answer
----------------------------------------------------------------------
Traceback (most recent call last):
...
assert answer == 42
AssertionError:
>> assert 43 == 42
так что в принципе тесты можно писать через assert без потери информативности вывода, нужно только использовать правильные "пускальщики".
Более краткие сигнатуры
from nose.tools import eq_
answer = 43
def test_answer():
eq_(answer, 42)
После запуска, вывод:
FAIL: test.test_answer
----------------------------------------------------------------------
Traceback (most recent call last):
...
eq_(answer, 42)
AssertionError: 43 != 42
Заменили assert_equal на более короткий вариант eq_, вывод ошибки будет полностью аналогичен. Т.е. при выводе увидим, что answer на самом деле 43 и пойдем сразу искать ошибку в коде. Один нюанс, что тесты не заканчиваются проверкой на eq_ и ok_, которые есть в nose.tools, набор методов нужен более обширный...
Интересное по теме
pytest - это аналог nose, со своими "плюшками", он умеет запускать большинство тестов написанных для nose.
attest - интересный подход (python way) от извесной команды Pocoo. Пример из документации:
from attest import Tests math = Tests() @math.test def arithmetics(): """Ensure that the laws of physics are in check.""" assert 1 + 1 == 2 if __name__ == '__main__': math.run()
Oktest для лаконичности - идея прикольная. Пример из документации:
from oktest import ok ok (x) > 0 # same as assert_(x > 0) ok (s) == 'foo' # same as assertEqual(s, 'foo') ok (s) != 'foo' # same as assertNotEqual(s, 'foo')
Итого
В python есть множество способов для написания и запуска тестов, в статье упоминаются не все. Если задаться целью, то можно писать красивые и лаконичные тесты.