Мокинг (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».