Рубрики
Без рубрики

unittest – Автоматизированная среда тестирования

Автор оригинала: 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 – популярный сторонний инструмент запуска тестов с поддержкой