<style>.lazy{display:none}</style>Основы работы с мокингом в Python

Основы работы с мокингом в Python

Введение

Мокинг (mocking) – это замена тестируемой части приложения фиктивной версией этой части, называемой мок (mock, от англ. “имитация”).

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

Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.

Каковы преимущества мокинга?

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

Предусловия

Вам потребуется установить 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

Этот класс реализует один метод  sum который принимает два аргумента – числа, которые нужно сложить,  a и 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 как есть каждый раз, когда мы запускаем тесты, явно не лучшая идея. Это та ситуация, когда мы можем использовать мокинг, чтобы ускорить тестирование и одновременно избежать нежелательного побочного эффекта.

Давайте отрефакторим тест-кейс так, чтобы вместо вызова 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».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *