Поиск…


замечания

Для Python существует несколько модулей тестирования. В этом разделе документации описывается базовый модуль unittest . Другие инструменты тестирования включают py.test и nosetests . В этой документации по тестированию 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 для проверки этого исключения. assertRaises можно использовать двумя способами:

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

Смысловые функции с 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 или нос:

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 в этих методах, чтобы также tearDown методы 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 если 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)

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

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

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

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

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)

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

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

В то время как у 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


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow