Автор оригинала: Doug Hellmann.
Цель:
Фреймворк автоматизированного тестирования
Модуль Python unittest
основан на конструкции фреймворка XUnit, разработанной Кентом Беком и Эрихом Гаммой. Тот же шаблон повторяется во многих других языках, включая C, Perl, Java и Smalltalk. Платформа, реализованная с помощью unittest
, поддерживает фикстуры, наборы тестов и средство выполнения тестов для обеспечения автоматического тестирования.
Базовая структура теста
Тесты, как определено в unittest
, состоят из двух частей: кода для управления зависимостями теста (так называемые fixture ) и самого теста. Индивидуальные тесты создаются путем создания подкласса TestCase
и переопределения или добавления соответствующих методов. В следующем примере SimplisticTest
имеет единственный метод test ()
, который завершится ошибкой, если a
когда-либо будет отличаться от b .
unittest_simple.py
import unittest class SimplisticTest(unittest.TestCase): def test(self): a 'a' b 'a' self.assertEqual(a, b)
Запуск тестов
Самый простой способ запустить модульное тестирование – использовать автоматическое обнаружение, доступное через интерфейс командной строки.
$ python3 -m unittest unittest_simple.py . ---------------------------------------------------------------- Ran 1 test in 0.000s OK
Эти сокращенные выходные данные включают количество времени, которое потребовалось для тестирования, а также индикатор состояния для каждого теста («.» В первой строке выходных данных означает, что тест пройден). Для получения более подробных результатов теста включите параметр -v
.
$ python3 -m unittest -v unittest_simple.py test (unittest_simple.SimplisticTest) ... ok ---------------------------------------------------------------- Ran 1 test in 0.000s OK
Результаты испытаний
Тесты имеют 3 возможных результата, описанных в таблице ниже.
Результаты теста
Исход
Описание
Ok
Тест пройден.
ПРОВАЛ
Тест не проходит и вызывает исключение AssertionError.
ОШИБКА
Тест вызывает любое исключение, кроме AssertionError.
Не существует явного способа заставить тест «пройти», поэтому статус теста зависит от наличия (или отсутствия) исключения.
unittest_outcomes.py
import unittest class OutcomesTest(unittest.TestCase): def testPass(self): return def testFail(self): self.assertFalse(True) def testError(self): raise RuntimeError('Test error!')
Когда тест не проходит или генерирует ошибку, в вывод включается трассировка.
$ python3 -m unittest unittest_outcomes.py EF. ERROR: testError (unittest_outcomes.OutcomesTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_outcomes.py", line 18, in testError raise RuntimeError('Test error!') RuntimeError: Test error! FAIL: testFail (unittest_outcomes.OutcomesTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_outcomes.py", line 15, in testFail self.assertFalse(True) AssertionError: True is not false ---------------------------------------------------------------- Ran 3 tests in 0.001s FAILED
В предыдущем примере функция testFail ()
завершилась ошибкой, и при трассировке отображается строка с кодом ошибки. Тем не менее, человек, читающий выходные данные теста, должен посмотреть на код, чтобы выяснить значение неудавшегося теста.
unittest_failwithmessage.py
import unittest class FailureMessageTest(unittest.TestCase): def testFail(self): self.assertFalse(True, 'failure message goes here')
Чтобы упростить понимание природы сбоя теста, все методы fail * ()
и assert * ()
принимают аргумент msg
, который можно использовать для создания более подробного сообщения об ошибке.
$ python3 -m unittest -v unittest_failwithmessage.py testFail (unittest_failwithmessage.FailureMessageTest) ... FAIL FAIL: testFail (unittest_failwithmessage.FailureMessageTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_failwithmessage.py", line 12, in testFail self.assertFalse(True, 'failure message goes here') AssertionError: True is not false : failure message goes here ---------------------------------------------------------------- Ran 1 test in 0.000s FAILED
Утверждение истины
Большинство тестов подтверждают истинность некоторых условий. Есть два разных способа написать тесты для проверки истинности, в зависимости от точки зрения автора теста и желаемого результата тестируемого кода.
unittest_truth.py
import unittest class TruthTest(unittest.TestCase): def testAssertTrue(self): self.assertTrue(True) def testAssertFalse(self): self.assertFalse(False)
Если код создает значение, которое можно оценить как истинное, следует использовать метод assertTrue ()
. Если код выдает ложное значение, метод assertFalse ()
имеет больше смысла.
$ python3 -m unittest -v unittest_truth.py testAssertFalse (unittest_truth.TruthTest) ... ok testAssertTrue (unittest_truth.TruthTest) ... ok ---------------------------------------------------------------- Ran 2 tests in 0.000s OK
Проверка равенства
В качестве особого случая unittest
включает методы для проверки равенства двух значений.
unittest_equality.py
import unittest class EqualityTest(unittest.TestCase): def testExpectEqual(self): self.assertEqual(1, 3 - 2) def testExpectEqualFails(self): self.assertEqual(2, 3 - 2) def testExpectNotEqual(self): self.assertNotEqual(2, 3 - 2) def testExpectNotEqualFails(self): self.assertNotEqual(1, 3 - 2)
В случае сбоя эти специальные методы тестирования выдают сообщения об ошибках, включая сравниваемые значения.
$ python3 -m unittest -v unittest_equality.py testExpectEqual (unittest_equality.EqualityTest) ... ok testExpectEqualFails (unittest_equality.EqualityTest) ... FAIL testExpectNotEqual (unittest_equality.EqualityTest) ... ok testExpectNotEqualFails (unittest_equality.EqualityTest) ... FAIL FAIL: testExpectEqualFails (unittest_equality.EqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality.py", line 15, in testExpectEqualFails self.assertEqual(2, 3 - 2) AssertionError: 2 FAIL: testExpectNotEqualFails (unittest_equality.EqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality.py", line 21, in testExpectNotEqualFails self.assertNotEqual(1, 3 - 2) AssertionError: 1 ---------------------------------------------------------------- Ran 4 tests in 0.001s FAILED
Почти равны?
В дополнение к строгому равенству можно проверить почти равенство чисел с плавающей запятой с помощью assertAlmostEqual ()
и assertNotAlmostEqual ()
.
unittest_almostequal.py
import unittest class AlmostEqualTest(unittest.TestCase): def testEqual(self): self.assertEqual(1.1, 3.3 - 2.2) def testAlmostEqual(self): self.assertAlmostEqual(1.1, 3.3 - 2.2, places1) def testNotAlmostEqual(self): self.assertNotAlmostEqual(1.1, 3.3 - 2.0, places1)
Аргументы – это сравниваемые значения и количество десятичных разрядов, используемых для теста.
$ python3 -m unittest unittest_almostequal.py .F. FAIL: testEqual (unittest_almostequal.AlmostEqualTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_almostequal.py", line 12, in testEqual self.assertEqual(1.1, 3.3 - 2.2) AssertionError: 1.1 ---------------------------------------------------------------- Ran 3 tests in 0.001s FAILED
Контейнеры
В дополнение к общим assertEqual ()
и assertNotEqual ()
существуют специальные методы для сравнения контейнеров, например list
, dict
и установить
объекты.
unittest_equality_container.py
import textwrap import unittest class ContainerEqualityTest(unittest.TestCase): def testCount(self): self.assertCountEqual( [1, 2, 3, 2], [1, 3, 2, 3], ) def testDict(self): self.assertDictEqual( {'a': 1, 'b': 2}, {'a': 1, 'b': 3}, ) def testList(self): self.assertListEqual( [1, 2, 3], [1, 3, 2], ) def testMultiLineString(self): self.assertMultiLineEqual( textwrap.dedent(""" This string has more than one line. """), textwrap.dedent(""" This string has more than two lines. """), ) def testSequence(self): self.assertSequenceEqual( [1, 2, 3], [1, 3, 2], ) def testSet(self): self.assertSetEqual( set([1, 2, 3]), set([1, 3, 2, 4]), ) def testTuple(self): self.assertTupleEqual( (1, 'a'), (1, 'b'), )
Каждый метод сообщает о неравенстве с использованием формата, который имеет значение для типа ввода, что упрощает понимание и исправление ошибок тестирования.
$ python3 -m unittest unittest_equality_container.py FFFFFFF FAIL: testCount (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 15, in testCount [1, 3, 2, 3], AssertionError: Element counts were not equal: First has 2, Second has 1: 2 First has 1, Second has 2: 3 FAIL: testDict (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 21, in testDict {'a': 1, 'b': 3}, AssertionError: {'a': 1, 'b': 2}a': 1, 'b': 3} - {'a': 1, 'b': 2} ? ^ + {'a': 1, 'b': 3} ? ^ FAIL: testList (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 27, in testList [1, 3, 2], AssertionError: Lists differ: [1, 2, 3] First differing element 1: 2 3 - [1, 2, 3] + [1, 3, 2] FAIL: testMultiLineString (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 41, in testMultiLineString """), AssertionError: '\nThis string\nhas more than one\nline.\n' != '\nThis string has\nmore than two\nlines.\n' - This string + This string has ? ++++ - has more than one ? ---- -- + more than two ? ++ - line. + lines. ? + FAIL: testSequence (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 47, in testSequence [1, 3, 2], AssertionError: Sequences differ: [1, 2, 3] First differing element 1: 2 3 - [1, 2, 3] + [1, 3, 2] FAIL: testSet (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 53, in testSet set([1, 3, 2, 4]), AssertionError: Items in the second set but not the first: 4 FAIL: testTuple (unittest_equality_container.ContainerEqualityTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_equality_container.py", line 59, in testTuple (1, 'b'), AssertionError: Tuples differ: (1, 'a') 'b') First differing element 1: 'a' 'b' - (1, 'a') ? ^ + (1, 'b') ? ^ ---------------------------------------------------------------- Ran 7 tests in 0.005s FAILED
Используйте assertIn ()
для проверки членства в контейнере.
unittest_in.py
import unittest class ContainerMembershipTest(unittest.TestCase): def testDict(self): self.assertIn(4, {1: 'a', 2: 'b', 3: 'c'}) def testList(self): self.assertIn(4, [1, 2, 3]) def testSet(self): self.assertIn(4, set([1, 2, 3]))
Любой объект, поддерживающий оператор in
или API контейнера, можно использовать с assertIn ()
.
$ python3 -m unittest unittest_in.py FFF FAIL: testDict (unittest_in.ContainerMembershipTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_in.py", line 12, in testDict self.assertIn(4, {1: 'a', 2: 'b', 3: 'c'}) AssertionError: 4 not found in {1: 'a', 2: 'b', 3: 'c'} FAIL: testList (unittest_in.ContainerMembershipTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_in.py", line 15, in testList self.assertIn(4, [1, 2, 3]) AssertionError: 4 not found in [1, 2, 3] FAIL: testSet (unittest_in.ContainerMembershipTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_in.py", line 18, in testSet self.assertIn(4, set([1, 2, 3])) AssertionError: 4 not found in {1, 2, 3} ---------------------------------------------------------------- Ran 3 tests in 0.001s FAILED
Проверка на исключения
Как упоминалось ранее, если тест вызывает исключение, отличное от AssertionError
, это рассматривается как ошибка. Это очень полезно для выявления ошибок при изменении кода, имеющего существующее тестовое покрытие. Однако существуют обстоятельства, при которых тест должен проверить, действительно ли какой-то код создает исключение. Например, если атрибуту объекта присвоено недопустимое значение. В таких случаях assertRaises ()
делает код более понятным, чем перехват исключения в тесте. Сравните эти два теста:
unittest_exception.py
import unittest def raises_error(*args, **kwds): raise ValueError('Invalid value: ' + str(args) + str(kwds)) class ExceptionTest(unittest.TestCase): def testTrapLocally(self): try: raises_error('a', b'c') except ValueError: pass else: self.fail('Did not see ValueError') def testAssertRaises(self): self.assertRaises( ValueError, raises_error, 'a', b'c', )
Результаты для обоих одинаковы, но второй тест с использованием assertRaises ()
более краток.
$ python3 -m unittest -v unittest_exception.py testAssertRaises (unittest_exception.ExceptionTest) ... ok testTrapLocally (unittest_exception.ExceptionTest) ... ok ---------------------------------------------------------------- Ran 2 tests in 0.000s OK
Тестовые приспособления
Приспособления – это внешние ресурсы, необходимые для тестирования. Например, всем тестам для одного класса может потребоваться экземпляр другого класса, который предоставляет параметры конфигурации или другой общий ресурс. Другие приспособления для тестирования включают подключения к базе данных и временные файлы (многие люди утверждают, что использование внешних ресурсов делает такие тесты не «модульными» тестами, но они по-прежнему являются тестами и по-прежнему полезны).
unittest
включает специальные перехватчики для настройки и очистки любых приспособлений, необходимых для тестов. Чтобы установить фикстуры для каждого отдельного тестового примера, переопределите setUp ()
в TestCase
. Чтобы очистить их, переопределите tearDown ()
. Чтобы управлять одним набором фикстур для всех экземпляров тестового класса, переопределите методы класса setUpClass ()
и tearDownClass ()
для TestCase
. А для обработки особенно дорогостоящих операций настройки для всех тестов в модуле используйте функции уровня модуля setUpModule ()
и tearDownModule ()
.
unittest_fixtures.py
import random import unittest def setUpModule(): print('In setUpModule()') def tearDownModule(): print('In tearDownModule()') class FixturesTest(unittest.TestCase): @classmethod def setUpClass(cls): print('In setUpClass()') cls.good_range range(1, 10) @classmethod def tearDownClass(cls): print('In tearDownClass()') del cls.good_range def setUp(self): super().setUp() print('\nIn setUp()') # Pick a number sure to be in the range. The range is # defined as not including the "stop" value, so make # sure it is not included in the set of allowed values # for our choice. self.value random.randint( self.good_range.start, self.good_range.stop - 1, ) def tearDown(self): print('In tearDown()') del self.value super().tearDown() def test1(self): print('In test1()') self.assertIn(self.value, self.good_range) def test2(self): print('In test2()') self.assertIn(self.value, self.good_range)
При запуске этого образца теста порядок выполнения приспособлений и методов тестирования очевиден.
$ python3 -u -m unittest -v unittest_fixtures.py In setUpModule() In setUpClass() test1 (unittest_fixtures.FixturesTest) ... In setUp() In test1() In tearDown() ok test2 (unittest_fixtures.FixturesTest) ... In setUp() In test2() In tearDown() ok In tearDownClass() In tearDownModule() ---------------------------------------------------------------- Ran 2 tests in 0.000s OK
Не все методы tearDown
могут быть вызваны, если в процессе очистки фикстур возникают ошибки. Чтобы обеспечить правильное освобождение фикстуры, используйте addCleanup ()
.
unittest_addcleanup.py
import random import shutil import tempfile import unittest def remove_tmpdir(dirname): print('In remove_tmpdir()') shutil.rmtree(dirname) class FixturesTest(unittest.TestCase): def setUp(self): super().setUp() self.tmpdir tempfile.mkdtemp() self.addCleanup(remove_tmpdir, self.tmpdir) def test1(self): print('\nIn test1()') def test2(self): print('\nIn test2()')
Этот пример теста создает временный каталог, а затем использует shutil для его очистки после завершения теста.
$ python3 -u -m unittest -v unittest_addcleanup.py test1 (unittest_addcleanup.FixturesTest) ... In test1() In remove_tmpdir() ok test2 (unittest_addcleanup.FixturesTest) ... In test2() In remove_tmpdir() ok ---------------------------------------------------------------- Ran 2 tests in 0.002s OK
Повторение тестов с разными входами
Часто бывает полезно запустить одну и ту же логику тестирования с разными входами. Вместо того, чтобы определять отдельный тестовый метод для каждого небольшого случая, общий способ сделать это – использовать один тестовый метод, содержащий несколько связанных вызовов утверждений. Проблема с этим подходом в том, что как только одно утверждение терпит неудачу, остальные пропускаются. Лучшее решение – использовать subTest ()
для создания контекста для теста в методе тестирования. Если тест не проходит, сообщается об ошибке, и остальные тесты продолжаются.
unittest_subtest.py
import unittest class SubTest(unittest.TestCase): def test_combined(self): self.assertRegex('abc', 'a') self.assertRegex('abc', 'B') # The next assertions are not verified! self.assertRegex('abc', 'c') self.assertRegex('abc', 'd') def test_with_subtest(self): for pat in ['a', 'B', 'c', 'd']: with self.subTest(patternpat): self.assertRegex('abc', pat)
В этом примере метод test_combined ()
никогда не выполняет утверждения для шаблонов 'c'
и 'd'
. Метод test_with_subtest ()
выполняет и правильно сообщает о дополнительной ошибке. Обратите внимание, что средство запуска тестов по-прежнему считает, что существует только два тестовых случая, даже если было зарегистрировано три сбоя.
$ python3 -m unittest -v unittest_subtest.py test_combined (unittest_subtest.SubTest) ... FAIL test_with_subtest (unittest_subtest.SubTest) ... FAIL: test_combined (unittest_subtest.SubTest) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_subtest.py", line 13, in test_combined self.assertRegex('abc', 'B') AssertionError: Regex didn't match: 'B' not found in 'abc' FAIL: test_with_subtest (unittest_subtest.SubTest)) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_subtest.py", line 21, in test_with_subtest self.assertRegex('abc', pat) AssertionError: Regex didn't match: 'B' not found in 'abc' FAIL: test_with_subtest (unittest_subtest.SubTest)) ---------------------------------------------------------------- Traceback (most recent call last): File ".../unittest_subtest.py", line 21, in test_with_subtest self.assertRegex('abc', pat) AssertionError: Regex didn't match: 'd' not found in 'abc' ---------------------------------------------------------------- Ran 2 tests in 0.001s FAILED
Пропуск тестов
Часто бывает полезно пропустить тест, если не выполняется какое-либо внешнее условие. Например, при написании тестов для проверки поведения библиотеки в определенной версии Python нет причин запускать эти тесты в других версиях Python. Классы и методы тестирования можно украсить с помощью skip ()
, чтобы всегда пропускать тесты. Декораторы skipIf ()
и skipUnless ()
можно использовать для проверки условия перед пропуском.
unittest_skip.py
import sys import unittest class SkippingTest(unittest.TestCase): @unittest.skip('always skipped') def test(self): self.assertTrue(False) @unittest.skipIf(sys.version_info[0] > 2, 'only runs on python 2') def test_python2_only(self): self.assertTrue(False) @unittest.skipUnless(sys.platform 'Darwin', 'only runs on macOS') def test_macos_only(self): self.assertTrue(True) def test_raise_skiptest(self): raise unittest.SkipTest('skipping via exception')
Для сложных условий, которые трудно выразить в одном выражении, которое нужно передать в skipIf ()
или skipUnless ()
, тестовый пример может вызвать SkipTest
непосредственно для пропуска теста.
$ python3 -m unittest -v unittest_skip.py test (unittest_skip.SkippingTest) ... skipped 'always skipped' test_macos_only (unittest_skip.SkippingTest) ... skipped 'only runs on macOS' test_python2_only (unittest_skip.SkippingTest) ... skipped 'only runs on python 2' test_raise_skiptest (unittest_skip.SkippingTest) ... skipped 'skipping via exception' ---------------------------------------------------------------- Ran 4 tests in 0.000s OK
Игнорирование неудачных тестов
Вместо того, чтобы удалять тесты, которые постоянно нарушаются, их можно пометить с помощью декоратора expectedFailure ()
, чтобы проигнорировать сбой.
unittest_expectedfailure.py
import unittest class Test(unittest.TestCase): @unittest.expectedFailure def test_never_passes(self): self.assertTrue(False) @unittest.expectedFailure def test_always_passes(self): self.assertTrue(True)
Если тест, который, как ожидается, завершится неудачей, действительно проходит, это состояние рассматривается как особый тип отказа и сообщается как «неожиданный успех».
$ python3 -m unittest -v unittest_expectedfailure.py test_always_passes (unittest_expectedfailure.Test) ... unexpected success test_never_passes (unittest_expectedfailure.Test) ... expected failure ---------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (expected
Смотрите также
- стандартная библиотека документации для unittest
- doctest – альтернативный способ запуска тестов, встроенный в строки документации или внешние файлы документации.
- нос – сторонняя программа для запуска тестов со сложными функциями обнаружения.
- pytest – популярный сторонний инструмент запуска тестов с поддержкой