DevGang
Авторизоваться

Имитация возвращаемых значений в Python

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

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

В основном вы будете принимать меры, чтобы справиться с неудачными запросами, но как написать модульные тесты, чтобы убедиться, что ваша система работает так, как ожидается?

Программа написания

Давайте создадим программу, которая пингует url и возвращает True, если код состояния равен 200, и False, если нет.

  1. Создайте файл client.py
  2. Напишите следующий код
# client.py

# Make sure you have requests installed.
# If not, you can install with `pip3 install requests` 
# or `pip install requests`.
import requests

def ping(url):
    res = requests.get(url)
    retun res.status_code == 200

print(ping("https://google.com"))

Запустите python3 client.py в терминале в директории, где находится client.py. Ниже должен появиться ответ:

❯ python3 client.py
True

Письменные тесты

Наша программа работает, как и ожидалось. Пришло время написать тесты, чтобы убедиться, что мы возвращаем True, когда наш запрос равен 200, и False, когда код состояния не равен 200.

  1. Создайте файл test_client.py в той же директории, что и client.py.
  2. Напишите следующий код:
# test_client.py
import unittest
from client import ping

class TestClient(unittest.TestCase):
    def setUp(self):
        self.url = "https://google.com"

    def test_ping_returns_200(self):
        result = ping(self.url)
        self.assertTrue(result)

if __name__ == "__main__":
    unittest.main()

Запустите файл python3 test_client.py. Вы должны получить ответ, как показано ниже:

❯ python3 test_client.py
.
---------------------------------------------------------
Ran 1 test in 1.113s

OK

Все наши тесты пройдены, но теперь давайте проверим случай, когда код состояния не равен 200. Давайте добавим еще один тест к нашему тесту:

# test_client.py
import unittest
from client import ping

class TestClient(unittest.TestCase):
    def setUp(self):
        self.url = "https://google.com"

    def test_ping_returns_200(self):
        result = ping(self.url)
        self.assertTrue(result)

    # New
     def test_ping_returns_500(self):
        result = ping(self.url)
        self.assertFalse(result)


if __name__ == "__main__":
    unittest.main()

При повторном запуске python3 test_client.py наш новый тест проваливается.


❯ python3 test_client.py
.F
==========================================================
FAIL: test_ping_returns_500 (__main__.TestClient.test_ping_returns_500)
----------------------------------------------------------
Traceback (most recent call last):
  File "/Users/mock/test_client.py", line 46, in test_ping_returns_500
    self.assertFalse(result)
AssertionError: True is not false

-----------------------------------------------------------
Ran 2 tests in 2.076s

FAILED (failures=1)

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

Чтобы сделать это, мы будем имитировать наши запросы и ответы. Это даст нам возможность определить, какой ответ мы должны получить.

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

Имитация запросов и ответов

Сначала мы импортируем декоратор патчей (patch) и объект MagicMock из unittest.mock.

Декоратор патча (patch) позволяет нам редактировать наши запросы (requests), он принимает цель, которая будет запросами, которые мы хотим изменить, в данном случае это запрос в client.py.

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

Мы добавили дополнительный параметр, который можно назвать как угодно. Я назову его mock_requests. Это объект, к которому мы можем прикрепить наш ответ.

Давайте добавим эти изменения в наш test_client.py:

# test_client.py
import unittest
from unittest.mock import patch, MagicMock
from client import ping

class TestClient(unittest.TestCase):
    def setUp(self):
        self.url = "https://google.com"

    def test_ping_returns_200(self):
        result = ping(self.url)
        self.assertTrue(result)


    @patch("client.requests")   # New
    def test_ping_returns_500(self, mock_requests):    # New

        # New
        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_requests.get.return_value = mock_response

        result = ping(self.url)
        self.assertFalse(result)


if __name__ == "__main__":
    unittest.main()

Давайте посмотрим, что мы уже успели сделать!

  1. Добавляя декоратор @patch("client.requests"), мы говорим, что хотим изменить функцию requests, которую мы импортировали в client.py. Это может быть любая функция, мы могли бы нацелиться на сам ping. После этого вы можете попробовать сделать это самостоятельно. Декоратор патча изменяет наш метод тестов и передает второй аргумент методу test_ping_returns_500. Таким образом, мы добавляем mock_requests в качестве параметра к нашему методу. Этот mock_requests представляет собой имитированный вариант requests.
  2. Мы инстанцируем наш объект MagicMock и назначаем его mock_response. Наш объект MagicMock позволяет нам принимать форму любого класса, который мы хотим, и в данном случае это объект Response.
  3. Мы добавляем атрибут status_code к нашему mock_response и присваиваем ему значение 500.
  4. Далее мы назначаем наш mock_response на mock_requests.get.return_value. Теперь мы изменили наши запросы таким образом, что, когда requests в client.py вызывают функцию get, мы хотим, чтобы возвращаемое значение было значением нашего mock_response. Таким образом, когда вызывается requests.get(), возвращаемое значение будет mock_response.

Если мы снова запустим наш тест, то увидим, что теперь все работает.


❯ python3 test_client.py
..
----------------------------------------------------------------------
Ran 2 tests in 1.609s

OK

Мы можем проделать то же самое для метода test_ping_returns_200, но сделать кодом статуса 200, чтобы мы могли добавить фальшивый url вместо запросов к https://google.com.

# test_client.py
import unittest
from unittest.mock import patch, MagicMock
from client import ping

class TestClient(unittest.TestCase):
    def setUp(self):
        self.url = "https://api.service.com"    # New

    @patch("client.requests")   # New
    def test_ping_returns_200(self, mock_requests):    # New

        # New
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_requests.get.return_value = mock_response


        result = ping(self.url)
        self.assertTrue(result)


    @patch("client.requests")
    def test_ping_returns_500(self, mock_requests):

        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_requests.get.return_value = mock_response

        result = ping(self.url)
        self.assertFalse(result)


if __name__ == "__main__":
    unittest.main()

Мы можем снова запустить наши тесты, чтобы убедиться, что всё работает хорошо.


❯ python3 test_client.py
..
----------------------------------------------------------------------
Ran 2 tests in 1.609s

OK

Тело ответа для имитации

Мы также можем добавить тело ответа в наш mock_response. Предположим, что сторонний API, который мы пингуем, возвращает json-ответ с атрибутом {"status": "ok"}.

Давайте изменим нашу функцию в client.py, чтобы она тоже возвращала json-ответ, если код статуса равен 200, и None, если нет.

# client.py

import requests

def ping(url):
    res = requests.get(url)
    # New
    if res.status_code == 200:
        return (True, res.json())
    return (False, None)

Изменим наш файл test_client.py следующим образом:

# test_client.py
import unittest
from unittest.mock import patch, MagicMock
from client import ping

class TestClient(unittest.TestCase):
    def setUp(self):
        self.url = "https://api.service.com"

    @patch("client.requests")
    def test_ping_returns_200(self, mock_requests):

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"status": "ok"}  # New
        mock_requests.get.return_value = mock_response


        status, body = ping(self.url)  # New
        self.assertTrue(status)
        self.assertEqual(body["status"], "ok")


    @patch("client.requests")
    def test_ping_returns_500(self, mock_requests):

        mock_response = MagicMock()
        mock_response.status_code = 500
        mock_requests.get.return_value = mock_response

        status, body = ping(self.url)  # New
        self.assertFalse(status)


if __name__ == "__main__":
    unittest.main()

Добавив mock_response.json.return_value, мы хотим сказать, что при вызове функции json на нашем mock_response мы должны получить {"status": "ok"}.

Таким образом, {"status": "ok"} - это значение, которое мы получим при вызове res.json() в client.py.

Заключение

Запросы с имитацией упрощают тестирование, особенно когда вам приходится иметь дело с API сторонних производителей и даже с другими сервисами в ваших микросервисах.

Допустим, у вас есть API, который сначала делает запрос к вашему auth-сервису, чтобы подтвердить наличие у пользователя определенных прав, прежде чем разрешить ему доступ к определенным ресурсам.

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

Код для этого урока можно найти здесь: https://github.com/quamejnr/python-mock-requests.

Источник:

#Python
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу