Мокинг (mocking) — это замена тестируемой части приложения фиктивной версией этой части, называемой мок (англ. mock — «имитация»). Вместо того чтобы вызывать реальную функцию, вы будете вызывать мок, а затем делать утверждения о том, что должно произойти.
Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.
Каковы преимущества мокинга?
- Увеличение скорости. Тесты, которые выполняются быстро, чрезвычайно полезны. Например, если у вас есть очень ресурсоемкая функция, мок этой функции позволит сократить ненужное использование ресурсов во время тестирования, что уменьшит время выполнения теста.
- Предотвращение нежелательных побочных эффектов во время тестирования. Если вы тестируете функцию, которая выполняет вызовы внешнего API, вы можете не захотеть выполнять реальный вызов API при каждом запуске тестов. Вам придется менять свой код каждый раз, когда API меняется, или могут быть какие-то ограничения по скорости, но мокинг поможет вам избежать этого.
Примечание редакции: также рекомендуем почитать статью «Мокинг данных с Jest».
Предусловия
Вам потребуется установить Python 3.3 или выше. Скачайте необходимую версию для вашей платформы здесь. В этом уроке будет использоваться версия 3.6.0.
После установки настройте виртуальную среду:
python3 -m venv venv
Активируйте виртуальную среду, выполнив команду:
source venv/bin/activate
После этого добавьте файл main.py, в котором будет находиться наш код, и файл test.py для наших тестов:
touch main.py test.py
Базовое использование
Для примера возьмем простой класс:
class Calculator:
def sum(self, a, b):
return a + b
Класс Calculator реализует один метод — sum, принимающий два числа в качестве аргументов. Эти числа метод складывает и возвращает их сумму — 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 как есть при каждом запуске тестов явно не лучшая идея. Это та ситуация, когда мы можем использовать мокинг, чтобы ускорить тестирование и одновременно избежать нежелательного побочного эффекта.
Давайте отрефакторим тест-кейс так, чтобы при выполнении теста вместо функции 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 заменяется её мок-версией. Запустив этот тест-кейс, мы получим следующий результат:
. _____________________________________________________________ Ran 1 test in 0.001s OK
Хотя сначала это может показаться неинтуитивным, помните, что мокинг позволяет вам предоставить так называемую «фальшивую» реализацию той части вашей системы, которую вы тестируете. Это дает большую гибкость при тестировании. В разделе «Побочные эффекты» вы увидите, как запустить пользовательскую функцию при вызове мока вместо жесткого кодирования возвращаемого значения.
Более сложный пример
В этом примере мы будем использовать библиотеку 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 повлечет за собой вызов API jsonplaceholder, API-сервис генератора JSON.
В нашем тесте мы хотим произвести мокинг вызовов API и проверить только то, что метод posts объекта Blog возвращает записи в блоге. Для этого нам нужно пропатчить все объекты 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 декорирована @patch. Когда функция декорируется с помощью @patch, возвращается мок класса, метода или функции, переданный в качестве целевого объекта @patch, который затем передается в качестве аргумента декорированной функции.
В этом случае @patch вызывается с целевым объектом main.Blog и возвращает мок, который передается в тестовую функцию как MockBlog. Важно отметить, что целевой объект должен быть импортируемым в том окружении, в котором вызывается @patch. В нашем случае импорт from main import Blog должен быть разрешен без ошибок.
Также обратите внимание, что MockBlog — это имя переменной для представления созданного мока, и оно может быть любым.
Вызов blog.posts() на нашем мок-объекте блога возвращает наш предопределенный JSON. Запуск тестов должен пройти успешно.
. _____________________________________________________________ Ran 1 test in 0.001s OK
Мок позволяет проверить, сколько раз он был вызван, с какими аргументами, и даже был ли он вообще вызван. Дополнительные примеры мы рассмотрим в следующем разделе.
Другие утверждения, которые мы можем сделать для моков
Используя предыдущий пример, мы можем сделать несколько полезных утверждений для нашего объекта 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.
Как уже упоминалось ранее, мок-объект позволяет нам проверить не только возвращаемое значение, но и то, как он был вызван и какие аргументы были переданы.
Мок-объекты также могут быть сброшены в первоначальное состояние, т.е. когда мок-объект еще не был вызван. Это особенно полезно, если вы хотите сделать несколько вызовов к вашему моку и чтобы каждый из них выполнялся на новом экземпляре мока.
Побочные эффекты
Это те вещи, которые вы хотите, чтобы происходили при вызове вашей мок-функции. Частыми примерами являются вызов другой функции или появление исключений.
Давайте вернемся к нашей функции sum. Что, если вместо жесткого кодирования возвращаемого значения запустить пользовательскую функцию sum? Наша пользовательская функция избавится от нежелательного длительного вызова 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
Заключение
В этой статье мы рассмотрели основы мокинга в Python. Мы разобрали использование декоратора @patch и побочных эффектов для обеспечения альтернативного поведения ваших моков.
Теперь вы должны уметь использовать встроенные в Python возможности мокинга для замены частей тестируемой системы, чтобы писать более качественные и быстрые тесты.
Для получения более подробной информации следует обратиться к официальным документам.
Перевод статьи «Getting Started with Mocking in Python».
