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