Python. Компактные тесты

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

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 answer = 42
 2 
 3 
 4 def test_answer():
 5     '''
 6     >>> answer
 7     42
 8     '''
 9     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 есть множество способов для написания и запуска тестов, в статье упоминаются не все. Если задаться целью, то можно писать красивые и лаконичные тесты.