Модульное (unit) тестирование

Введение

Примеры

  • 1

    Тестирование исключений

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

     class WrongInputException(Exception):
        pass
    
     

    Это исключение возникает, когда вводится неправильный ввод, в следующем контексте, где мы всегда ожидаем число в качестве ввода текста.

     def convert2number(random_input):
        try:
            my_input = int(random_input)
        except ValueError:
            raise WrongInputException("Expected an integer!")
        return my_input
    
     

    Для того, чтобы проверить , было ли поднято исключение, мы используем assertRaises для проверки этого исключения. assertRaises можно использовать двумя способами:

    1. Используя обычный вызов функции. Первый аргумент принимает тип исключения, второй - вызываемый (обычно это функция), а остальные аргументы передаются этому вызываемому.
    2. Использование with пунктом, давая только тип исключения для функции. Преимущество этого заключается в том, что можно выполнять больше кода, но его следует использовать с осторожностью, поскольку несколько функций могут использовать одно и то же исключение, которое может быть проблематичным. Пример: с self.assertRaises (WrongInputException):
       convert2number("not a number") 

    Сначала это было реализовано в следующем тестовом примере:

     import unittest
    
    class ExceptionTestCase(unittest.TestCase):
    
        def test_wrong_input_string(self):
            self.assertRaises(WrongInputException, convert2number, "not a number")
    
        def test_correct_input(self):
            try:
                result = convert2number("56")
                self.assertIsInstance(result, int)
            except WrongInputException:
                self.fail()
    
     

    Также может возникнуть необходимость проверить исключение, которое не должно быть выброшено. Тем не менее, тест автоматически завершится неудачей, когда возникнет исключение, и, следовательно, может не потребоваться вообще. Просто чтобы показать варианты, второй метод тестирования показывает случай, когда можно проверить исключение, которое не должно быть выброшено. В основном, это поймать исключение , а затем проваливать испытание с использованием fail методы.

  • 0

    Перемешивание функций с помощью unittest.mock.create_autospec

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

    С функцией multiply в custom_math.py :

     def multiply(a, b):
        return a * b
    
     

    А функция multiples_of в process_math.py :

     from custom_math import multiply
    
    
    def multiples_of(integer, *args, num_multiples=0, **kwargs):
        """
        :rtype: list
        """
        multiples = []
    
        for x in range(1, num_multiples + 1):
            """
            Passing in args and kwargs here will only raise TypeError if values were 
            passed to multiples_of function, otherwise they are ignored. This way we can 
            test that multiples_of is used correctly. This is here for an illustration
            of how create_autospec works. Not recommended for production code.
            """
            multiple = multiply(integer,x, *args, **kwargs)
            multiples.append(multiple)
    
        return multiples
    
    
     

    Мы можем проверить multiples_of в одиночку, насмехаясь над из multiply . В приведенном ниже примере используется стандартная библиотека Python unittest, но это можно использовать и с другими средами тестирования, такими как pytest или nose:

     from unittest.mock import create_autospec
    import unittest
    
    # we import the entire module so we can mock out multiply
    import custom_math 
    custom_math.multiply = create_autospec(custom_math.multiply)
    from process_math import multiples_of
    
    
    class TestCustomMath(unittest.TestCase):
        def test_multiples_of(self):
            multiples = multiples_of(3, num_multiples=1)
            custom_math.multiply.assert_called_with(3, 1)
    
        def test_multiples_of_with_bad_inputs(self):
            with self.assertRaises(TypeError) as e:
                multiples_of(1, "extra arg",  num_multiples=1) # this should raise a TypeError 
  • 8

    Тестовая настройка и разрушение в пределах unittest.TestCase

    Иногда мы хотим подготовить контекст для каждого запускаемого теста. setUp метод запускается перед каждым испытанием в классе. tearDown запускается в конце каждого теста. Эти методы не являются обязательными. Помните , что TestCases часто используется в кооперативном множественном наследовании , так что вы должны быть осторожны , чтобы всегда вызывать super в этих методах , так что базовый класс setUp и tearDown метода также дозвонилась. Базовая реализация TestCase обеспечивает пустую setUp и tearDown методу , чтобы их можно было бы назвать , не поднимая исключение:

    import unittest
    
    
    class SomeTest(unittest.TestCase):
        def setUp(self):
            super(SomeTest, self).setUp()
            self.mock_data = [1,2,3,4,5]
    
        def test(self):
            self.assertEqual(len(self.mock_data), 5)
    
        def tearDown(self):
            super(SomeTest, self).tearDown()
            self.mock_data = []
    
    
    if __name__ == '__main__':
        unittest.main()
    
    

    Обратите внимание , что в python2.7 +, есть также addCleanup метод , который регистрирует функцию вызываться после выполнения теста. В отличие от tearDown который только вызывается , если setUp преуспевает, функции , зарегистрированные с помощью addCleanup будет называться даже в случае необработанного исключения в setUp . В качестве конкретного примера этот метод часто можно увидеть, удаляя различные макеты, которые были зарегистрированы во время выполнения теста:

     import unittest
    import some_module
    
    
    class SomeOtherTest(unittest.TestCase):
        def setUp(self):
            super(SomeOtherTest, self).setUp()
    
            # Replace `some_module.method` with a `mock.Mock`
            my_patch = mock.patch.object(some_module, 'method')
            my_patch.start()
    
            # When the test finishes running, put the original method back.
            self.addCleanup(my_patch.stop)
    
     

    Еще одно преимущество регистрации ыборкы таким образом, что она позволяет программисту поставить очищающий код рядом с кодом установки и защищает вас в том случае, если subclasser забывает назвать super в tearDown .

  • 3

    Утверждение об исключениях

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

    Использование менеджера контекста

     def division_function(dividend, divisor):
        return dividend / divisor
    
    
    class MyTestCase(unittest.TestCase):
        def test_using_context_manager(self):
            with self.assertRaises(ZeroDivisionError):
                x = division_function(1, 0)
    
     

    Это запустит код внутри диспетчера контекста и, в случае успеха, провалит тест, поскольку исключение не было вызвано. Если код выдает исключение правильного типа, тест будет продолжен.

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

     class MyTestCase(unittest.TestCase):
        def test_using_context_manager(self):
            with self.assertRaises(ZeroDivisionError) as ex:
                x = division_function(1, 0)
    
            self.assertEqual(ex.message, 'integer division or modulo by zero')
    
    
     

    Предоставляя вызываемую функцию

     def division_function(dividend, divisor):
        """
        Dividing two numbers.
    
        :type dividend: int
        :type divisor: int
    
        :raises: ZeroDivisionError if divisor is zero (0).
        :rtype: int
        """
        return dividend / divisor
    
    
    class MyTestCase(unittest.TestCase):
        def test_passing_function(self):
            self.assertRaises(ZeroDivisionError, division_function, 1, 0)
    
     

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

  • 1

    Выбор утверждений в рамках юнит-тестов

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

    Может быть , самое простое утверждение assertTrue , который может быть использован , как это:

     import unittest
    
    class SimplisticTest(unittest.TestCase):
        def test_basic(self):
            self.assertTrue(1 + 1 == 2)
    
     

    Это будет работать нормально, но заменив строку выше

             self.assertTrue(1 + 1 == 3)
    
     

    не удастся.

    assertTrue утверждение вполне вероятно , наиболее общее утверждение, так как что - то испытания могут быть отлиты как некоторые логическое условие, но часто есть лучшие альтернативы. При проверке на равенство, как указано выше, лучше написать

             self.assertEqual(1 + 1, 3)
    
     

    Когда первое не удается, сообщение

     ======================================================================
    
    FAIL: test (__main__.TruthTest)
    
    ----------------------------------------------------------------------
    
    Traceback (most recent call last):
    
      File "stuff.py", line 6, in test
    
        self.assertTrue(1 + 1 == 3)
    
    AssertionError: False is not true
    
     

    но когда последний терпит неудачу, сообщение

     ======================================================================
    
    FAIL: test (__main__.TruthTest)
    
    ----------------------------------------------------------------------
    
    Traceback (most recent call last):
    
      File "stuff.py", line 6, in test
    
        self.assertEqual(1 + 1, 3)
    AssertionError: 2 != 3
    
     

    который является более информативным (он фактически оценил результат левой стороны).

    Вы можете найти список утверждений в стандартной документации . В целом, хорошая идея - выбрать утверждение, наиболее точно соответствующее условию. Таким образом, как показано выше, утверждать , что 1 + 1 == 2 , лучше использовать assertEqual , чем assertTrue . Точно так же, утверждать , что a is None , то лучше использовать assertIsNone , чем assertEqual .

    Отметим также, что утверждения имеют отрицательные формы. Таким образом assertEqual имеет отрицательное партнерское assertNotEqual и assertIsNone имеет отрицательное партнерское assertIsNotNone . Еще раз, использование отрицательных аналогов при необходимости приведет к более четким сообщениям об ошибках.

  • 0

    Юнит тесты с pytest

    установка pytest:

     pip install pytest
    
     

    подготовка тестов:

     mkdir tests
    touch tests/test_docker.py
    
     

    Функции для тестирования в docker_something/helpers.py :

     from subprocess import Popen, PIPE 
    # this Popen is monkeypatched with the fixture `all_popens`
    
    def copy_file_to_docker(src, dest):
        try:
            result = Popen(['docker','cp', src, 'something_cont:{}'.format(dest)], stdout=PIPE, stderr=PIPE)
            err = result.stderr.read()
            if err:
                raise Exception(err)
        except Exception as e:
            print(e)
        return result
    
    def docker_exec_something(something_file_string):
        fl = Popen(["docker", "exec", "-i", "something_cont", "something"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
        fl.stdin.write(something_file_string)
        fl.stdin.close()
        err = fl.stderr.read()
        fl.stderr.close()
        if err:
            print(err)
            exit()
        result = fl.stdout.read()
        print(result)
    
    
     

    Импорт тестов test_docker.py :

     import os
    from tempfile import NamedTemporaryFile
    import pytest
    from subprocess import Popen, PIPE
    
    from docker_something import helpers
    copy_file_to_docker = helpers.copy_file_to_docker
    docker_exec_something = helpers.docker_exec_something
    
    
     

    насмешливый файл как объект в test_docker.py :

     class MockBytes():
        '''Used to collect bytes
        '''
        all_read = []
        all_write = []
        all_close = []
    
        def read(self, *args, **kwargs):
            # print('read', args, kwargs, dir(self))
            self.all_read.append((self, args, kwargs))
    
        def write(self, *args, **kwargs):
            # print('wrote', args, kwargs)
            self.all_write.append((self, args, kwargs))
    
        def close(self, *args, **kwargs):
            # print('closed', self, args, kwargs)
            self.all_close.append((self, args, kwargs))
    
        def get_all_mock_bytes(self):
            return self.all_read, self.all_write, self.all_close
    
     

    Обезьяна заплат с pytest в test_docker.py :

     @pytest.fixture
    def all_popens(monkeypatch):
        '''This fixture overrides / mocks the builtin Popen
            and replaces stdin, stdout, stderr with a MockBytes object
    
            note: monkeypatch is magically imported
        '''
        all_popens = []
    
        class MockPopen(object):
            def __init__(self, args, stdout=None, stdin=None, stderr=None):
                all_popens.append(self)
                self.args = args
                self.byte_collection = MockBytes()
                self.stdin = self.byte_collection
                self.stdout = self.byte_collection
                self.stderr = self.byte_collection
                pass
        monkeypatch.setattr(helpers, 'Popen', MockPopen)
    
        return all_popens
    
     

    Пример испытания, должны начинаться с префикса test_ в test_docker.py файле:

     def test_docker_install():
        p = Popen(['which', 'docker'], stdout=PIPE, stderr=PIPE)
        result = p.stdout.read()
        assert 'bin/docker' in result
    
    def test_copy_file_to_docker(all_popens):    
        result = copy_file_to_docker('asdf', 'asdf')
        collected_popen = all_popens.pop()
        mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
        assert mock_read
        assert result.args == ['docker', 'cp', 'asdf', 'something_cont:asdf']
    
    
    def test_docker_exec_something(all_popens):
    
        docker_exec_something(something_file_string)
    
        collected_popen = all_popens.pop()
        mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
        assert len(mock_read) == 3
        something_template_stdin = mock_write[0][1][0]
        these = [os.environ['USER'], os.environ['password_prod'], 'table_name_here', 'test_vdm', 'col_a', 'col_b', '/tmp/test.tsv']
        assert all([x in something_template_stdin for x in these])
    
    
     

    запуск тестов по одному:

     py.test -k test_docker_install tests
    py.test -k test_copy_file_to_docker tests
    py.test -k test_docker_exec_something tests
    
     

    выполнения всех тестов в tests папке:

     py.test -k test_ tests 

Синтаксис

Параметры

Примечания