Предисловие

Mock — это просто объект, который создает пустой тест для определенной части программы.

Вместо вызова обычной проверки, вы вызываете mock и смотрите, как проходит тест какая-то часть программы.

Какие преимущества имеет mock?

  • Высокая скорость

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

  • Избежание нежелательных побочных эффектов во время тестирования

Если вы тестируете функцию, которая вызывает внешний API, скорее всего вам не особо хочется каждый раз вызывать API для того, чтобы запустить тест. Вам придется менять код каждый раз, когда изменяется API, или могут быть некоторые ограничения скорости, но mock помогает этого избежать.

Что нужно, для того чтобы использовать mock?

Для начала вам нужно будет установить Python версии 3.3 или выше. Скачать Python можно здесь. Для этого урока я буду использовать Python версии 3.6.0.

После установки настройте виртуальную среду:

python3 -m venv mocking

Активируйте виртуальную среду:

source mocking/bin/activate

После этого добавьте файл main.py, в котором будет находиться наш код и файл test.py для наших тестов:

touch main.py test.py

Основы

Представьте себе самый обычный класс:

class Calculator:
    def sum(self, a, b):
        return a + b

Этот класс реализует сумму двух чисел, которая принимает два аргумента, а и b, а возвращает a + b;

Простой тест для этого класса может быть таким:

from unittest import TestCase
from main import Calculator

class TestCalculator(TestCase):
    def setUp(self):
        self.calc = Calculator()

    def test_sum(self):
        answer = self.calc.sum(2, 4)
        self.assertEqual(answer, 6)

Вы можете запустить этот тест, используя команду:

python -m unittest

На выходе вы увидите что-то похожее на это:

.
_____________________________________________________________

Ran 1 test in 0.003s

OK

Довольно быстро, не так ли?

Теперь представьте себе код, который выглядит вот так:

import time

class Calculator:
    def sum(self, a, b):
        time.sleep(10) # long running process
        return a + b

Так как это простой пример, мы используем time.sleep() для имитации продолжительного процесса. Предыдущий тест теперь выдает следующее:

.
_____________________________________________________________

Ran 1 test in 10.003s

OK

Этот процесс существенно замедлил наши тесты. Это явно не самая лучшая идея — вызывать sum каждый раз, когда мы проводим тесты. Это ситуация, когда мы можем использовать mock, чтобы ускорить тесты и избежать нежелательных последствий.

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

from unittest import TestCase
from unittest.mock import patch

class TestCalculator(TestCase):
    @patch('main.Calculator.sum', return_value=9)
    def test_sum(self, sum):
        self.assertEqual(sum(2,3), 9)

Мы импортируем декоратор patch из unittest.mock. Он заменяет фактическую функцию sum ложной функцией, которая ведет себя именно так, как мы хотим. В этом случае наша фиктивная функция всегда возвращает 9. В течение всего теста функция sum заменяется на mock. Запустив этот тест, мы получаем такие выходные данные:

.
_____________________________________________________________

Ran 1 test in 0.001s

OK

Сначала это может показаться интуитивно понятным, помните, что mock позволяет обеспечить так называемую поддельную реализацию той части вашей программы, которую вы тестируете. Это дает вам больше «гибкости» во время проведения тестов.

Более продвинутый пример использования

В этом примере мы будем использовать библиотеку requests, чтобы мы могли вызывать API. Вы можете установить ее используя pip install

pip install requests

Наш код под тест в main.py выглядит следующим образом:

import requests

class Blog:
    def __init__(self, name):
        self.name = name

    def posts(self):
        response = requests.get("https://jsonplaceholder.typicode.com/posts")

        return response.json()

    def __repr__(self):
        return '<Blog: {}>'.format(self.name)

Этот код определяет класс Blog с методом posts. Запустив posts в Blog, вы инициируете вызов API. Ссылаясь на post в Blog, объект будет инициировать вызов API jsonplaceholder.

В данном тесте мы хотим имитировать непредвиденный вызов API и проверить, что функция posts объекта Blog возвращает posts. Нам нужно будет исправить все posts объекта Blog следующим образом.

from unittest import TestCase
from unittest.mock import patch, Mock


class TestBlog(TestCase):
    @patch('main.Blog')
    def test_blog_posts(self, MockBlog):
        blog = MockBlog()

        blog.posts.return_value = [
            {
                'userId': 1,
                'id': 1,
                'title': 'Test Title',
                'body': 'Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the Galaxy\ lies a small unregarded yellow sun.'
            }
        ]

        response = blog.posts()
        self.assertIsNotNone(response)
        self.assertIsInstance(response[0], dict)

Вы можете обратить внимание, что функция test_blog_posts украшена декоратором (часть 1, часть 2) @patch. Когда функция оформлена через @patch, mock класса, метода или функции, переданная в качестве цели для @patch, возвращается и передается в качестве аргумента декорируемой функции.

В этом случае @patch вызывается с помощью main.Blog и возвращает mock, который передается функции теста как MockBlog. Важно отметить, что цель, перешедшая к @patch, должна быть импортирована в @patch, из которой она была вызвана. В нашем случае импорт формы from main import Blog должен быть разрешен без каких либо проблем.

Кроме того, обратите внимание, что MockBlog является обычной переменной и вы можете назвать её, как хотите.

Вызов blog.posts() на нашем ложном объекте блога возвращает предопределенный JSON.

.
_____________________________________________________________

Ran 1 test in 0.001s

OK

Обратите внимание, что тестирование mock вместо фактического объекта блога, позволяет нам делать дополнительные утверждения о том, как mock использовался.

Например, mock позволяет проверить, сколько раз он вызывался, аргументы, с которыми он вызывался, и даже был ли mock вообще когда либо вызван. Дополнительные примеры мы увидим в следующем разделе.

Другие примеры, как мы можем использовать mock

Используя предыдущий пример, мы можем сделать несколько полезных утверждений о нашем объекте mock blog.

import main

from unittest import TestCase
from unittest.mock import patch


class TestBlog(TestCase):
    @patch('main.Blog')
    def test_blog_posts(self, MockBlog):
        blog = MockBlog()

        blog.posts.return_value = [
            {
                'userId': 1,
                'id': 1,
                'title': 'Test Title,
                'body': 'Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the Galaxy\ lies a small unregarded yellow sun.'
            }
        ]

        response = blog.posts()
        self.assertIsNotNone(response)
        self.assertIsInstance(response[0], dict)

        # Additional assertions
        assert MockBlog is main.Blog # The mock is equivalent to the original

        assert MockBlog.called # The mock wasP called

        blog.posts.assert_called_with() # We called the posts method with no arguments

        blog.posts.assert_called_once_with() # We called the posts method once with no arguments

        # blog.posts.assert_called_with(1, 2, 3) - This assertion is False and will fail since we called blog.posts with no arguments

        blog.reset_mock() # Reset the mock object

        blog.posts.assert_not_called() # After resetting, posts has not been called.

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

Фиктивные объекты также могут быть и в нетронутом состоянии, т. е. mock объекта еще не был вызван. Это особенно полезно, когда вы хотите сделать несколько вызовов к mock и хотите, чтобы каждый из них работал на новом экземпляре mock.

Побочные эффекты

Давайте вернемся к нашей функции sum. Что делать, если вместо жесткого кодирования возвращаемого значения мы хотим запустить измененную функцию sum? Наша измененная функция mock будет долго выполнять time.sleep(), что нежелательно для нас, и останется только с изначальной функциональностью (суммой), которую мы хотим проверить. Мы можем просто определить side_effect в нашем тесте.

from unittest import TestCase
from unittest.mock import patch

def mock_sum(a, b):
    # mock sum function without the long running time.sleep
    return a + b

class TestCalculator(TestCase):
    @patch('main.Calculator.sum', side_effect=mock_sum)
    def test_sum(self, sum):
        self.assertEqual(sum(2,3), 5)
        self.assertEqual(sum(7,3), 10)

Вот что тест должен вывести:

.
_____________________________________________________________

Ran 1 test in 0.001s

OK

Заключение

В этой статье мы рассмотрели основы mock в Python. Теперь вы знаете, как использовать встроенные возможности Python для замены частей тестируемой системы, чтобы писать более быстрые тесты.

Перевод статьи Amos Omondi: Getting Started with Mocking in Python