В прошлый раз мы построили факториальную функцию в стиле, управляемом тестами, используя оператор assert Python:

def factorial(n):
    if n < 0:
        raise ValueError('Factorial of negative is undefined')
    return n * factorial(n-1) if n else 1
assert factorial(0) == 1
assert factorial(2) == 2
assert factorial(5) == 120
try:
    factorial(-1)
    assert False, 'Factorial of < 0 should have raised an error'
except ValueError as e:
    assert str(e) == 'Factorial of negative is undefined'

Однако это не тот способ, которым операторы утверждения обычно появляются в коде; чаще они встраиваются в программу как способ проверки инвариантов. Например, нам может быть не важно, какой тип исключения выдает наша факториальная функция при задании отрицательных чисел, поэтому мы можем немного упростить:

def factorial(n):
      assert n >= 0
      return n * factorial(n-1) if n else 1

Напротив, при написании тестов вместо того, чтобы писать «assert» в тестируемом модуле, мы чаще вытаскиваем тесты из основного тела программы и запускаем их с помощью инструмента тестирования. Делая это с нашим фактором факториала, мы получаем два файла: один для тестируемого кода и один для тестов; мы назовем их factorial.py и test_factorial.py.

модуль юниттест

Если мы скопируем наш первый тест в test_factorial.py, он будет выглядеть так:

from factorial import factorial
assert factorial(0) == 1

Возвращаясь к самому началу — до того, как мы вообще написали какой-либо код в нашей факториальной функции — мы запустим этот тест и увидим, что он не работает:

python test_factorial.py
Traceback (most recent call last):
  File "test_factorial.py", line 3, in <module>
    assert factorial(0) == 1
AssertionError

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

import unittest
from factorial import factorial
class TestFactorial(unittest.TestCase):
    def test_nothing(self):
        self.assertEqual(factorial(0), 1)
unittest.main()

Запуск этой версии расскажет нам больше об успехах и неудачах. Что наиболее важно, использование assertEqual() вместо оператора assert автоматически сообщает нам, какие значения идут с каждой стороны равенства. Теперь мы можем видеть, какое утверждение не удалось, и почему оно не удалось; здесь это потому, что наша факториальная функция вернула None:

$ python test_factorial.py
F
====================================================================
FAIL: test_nothing (__main__.TestFactorial)
--------------------------------------------------------------------
Traceback (most recent call last):
  File "test_factorial.py", line 7, in test_nothing
    self.assertEqual(factorial(0), 1)
AssertionError: None != 1
--------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)

Если мы исправим нашу функцию, чтобы этот тест прошел, unittest сообщит нам, что все в порядке:

$ python test_factorial.py
.
--------------------------------------------------------------------Ran 1 test in 0.000s
OK

Мы могли бы по-разному переводить наши другие тесты в версии юниттестов. Например, мы могли бы поместить все наши утверждения в одну функцию:

class TestFactorial(unittest.TestCase):
    def test_everything(self):
        self.assertEqual(factorial(0), 1)
        self.assertEqual(factorial(2), 2)
        self.assertEqual(factorial(5), 120)

Или мы могли бы разделить их на отдельные функции:

class TestFactorial(unittest.TestCase):
    def test_base_case(self):
        self.assertEqual(factorial(0), 1)
    def test_first_recursive_case(self):
        self.assertEqual(factorial(2), 2)
    def test_recursing_further(self):
        self.assertEqual(factorial(5), 120)

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

Рассмотрим, что происходит, когда мы переписываем нашу факториальную функцию в итеративную версию и предположим, что мы работаем в команде, которая имеет странное правило, согласно которому все переменные должны начинаться с нуля, поэтому мы напишем это так:

def factorial(n):
    acc = 0
    for x in range(n):
        acc = (acc or 1) * (x + 1)
    return acc

Мы запускаем наши тесты и обнаруживаем, что допустили ошибку:

python test_factorial.py
F..
====================================================================
FAIL: test_base_case (__main__.TestFactorial)
--------------------------------------------------------------------
Traceback (most recent call last):
  File "test_factorial.py", line 7, in test_base_case
    self.assertEqual(factorial(0), 1)
AssertionError: 0 != 1
--------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)

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

Сравните с тем, что произошло бы, если бы мы допустили большую ошибку, которая привела бы к провалу всех тестов:

$ python test_factorial.py
FFF
====================================================================
FAIL: test_base_case (__main__.TestFactorial)
...
====================================================================
FAIL: test_first_recursive_case (__main__.TestFactorial)
...
====================================================================
FAIL: test_recursing_further (__main__.TestFactorial)
...
Ran 3 tests in 0.001s
FAILED (failures=3)

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

В следующий раз мы рассмотрим еще пару жизненно важных функций тестировщика.