Документация по Python

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

В: Документация по Python

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

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

 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):

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

 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 методы.

Перемешивание функций с помощью 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 

Тестовая настройка и разрушение в пределах 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 .

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

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

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

 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)

 

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

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

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

Юнит тесты с 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 
Еще от кодкамп
Замечательно! Вы успешно подписались.
Добро пожаловать обратно! Вы успешно вошли
Вы успешно подписались на кодкамп.
Срок действия вашей ссылки истек.
Ура! Проверьте свою электронную почту на наличие волшебной ссылки для входа.
Успех! Ваша платежная информация обновлена.
Ваша платежная информация не была обновлена.