bots: Extract request_test_lib.py.

The new module mocks out calls to the requests library, which
is used by many of our bots that use third party services.

Mocking of requests.py is mostly orthogonal to our other
testing concerns.
This commit is contained in:
Steve Howell 2017-12-11 09:05:06 -06:00
parent fb8ee29bc1
commit 5034220829
2 changed files with 86 additions and 76 deletions

View file

@ -0,0 +1,81 @@
import json
import requests
from contextlib import contextmanager
from mock import patch
from typing import Any, Dict, List
@contextmanager
def mock_http_conversation(http_data):
# type: (Dict[str, Any]) -> Any
"""
Use this context manager to mock and verify a bot's HTTP
requests to the third-party API (and provide the correct
third-party API response. This allows us to test things
that would require the Internet without it).
http_data should be fixtures data formatted like the data
in zulip_bots/zulip_bots/bots/giphy/fixtures/test_normal.json
"""
def get_response(http_response, http_headers):
# type: (Dict[str, Any], Dict[str, Any]) -> Any
"""Creates a fake `requests` Response with a desired HTTP response and
response headers.
"""
mock_result = requests.Response()
mock_result._content = json.dumps(http_response).encode() # type: ignore # This modifies a "hidden" attribute.
mock_result.status_code = http_headers.get('status', 200)
return mock_result
def assert_called_with_fields(mock_result, http_request, fields):
# type: (Any, Dict[str, Any], List[str]) -> None
"""Calls `assert_called_with` on a mock object using an HTTP request.
Uses `fields` to determine which keys to look for in HTTP request and
to test; if a key is in `fields`, e.g., 'headers', it will be used in
the assertion.
"""
args = {}
for field in fields:
if field in http_request:
args[field] = http_request[field]
mock_result.assert_called_with(http_request['api_url'], **args)
http_request = http_data.get('request')
http_response = http_data.get('response')
http_headers = http_data.get('response-headers')
http_method = http_request.get('method', 'GET')
if http_method == 'GET':
with patch('requests.get') as mock_get:
mock_get.return_value = get_response(http_response, http_headers)
yield
assert_called_with_fields(
mock_get,
http_request,
['params', 'headers']
)
else:
with patch('requests.post') as mock_post:
mock_post.return_value = get_response(http_response, http_headers)
yield
assert_called_with_fields(
mock_post,
http_request,
['params', 'headers', 'json']
)
@contextmanager
def mock_request_exception():
# type: () -> Any
def assert_mock_called(mock_result):
# type: (Any) -> None
mock_result.assert_called()
with patch('requests.get') as mock_get:
mock_get.return_value = True
mock_get.side_effect = requests.exceptions.RequestException
yield
assert_mock_called(mock_get)

View file

@ -1,16 +1,19 @@
import json
import mock
import os
import requests
from mock import patch
from contextlib import contextmanager
from importlib import import_module
from unittest import TestCase
from typing import List, Dict, Any, Tuple
from zulip_bots.request_test_lib import (
mock_http_conversation,
mock_request_exception,
)
from zulip_bots.simple_lib import (
SimpleStorage,
SimpleMessageServer,
@ -185,77 +188,3 @@ def read_bot_fixture_data(bot_name, test_name):
content = f.read()
http_data = json.loads(content)
return http_data
@contextmanager
def mock_http_conversation(http_data):
# type: (Dict[str, Any]) -> Any
"""
Use this context manager to mock and verify a bot's HTTP
requests to the third-party API (and provide the correct
third-party API response. This allows us to test things
that would require the Internet without it).
http_data should be fixtures data formatted like the data
in zulip_bots/zulip_bots/bots/giphy/fixtures/test_normal.json
"""
def get_response(http_response, http_headers):
# type: (Dict[str, Any], Dict[str, Any]) -> Any
"""Creates a fake `requests` Response with a desired HTTP response and
response headers.
"""
mock_result = requests.Response()
mock_result._content = json.dumps(http_response).encode() # type: ignore # This modifies a "hidden" attribute.
mock_result.status_code = http_headers.get('status', 200)
return mock_result
def assert_called_with_fields(mock_result, http_request, fields):
# type: (Any, Dict[str, Any], List[str]) -> None
"""Calls `assert_called_with` on a mock object using an HTTP request.
Uses `fields` to determine which keys to look for in HTTP request and
to test; if a key is in `fields`, e.g., 'headers', it will be used in
the assertion.
"""
args = {}
for field in fields:
if field in http_request:
args[field] = http_request[field]
mock_result.assert_called_with(http_request['api_url'], **args)
http_request = http_data.get('request')
http_response = http_data.get('response')
http_headers = http_data.get('response-headers')
http_method = http_request.get('method', 'GET')
if http_method == 'GET':
with patch('requests.get') as mock_get:
mock_get.return_value = get_response(http_response, http_headers)
yield
assert_called_with_fields(
mock_get,
http_request,
['params', 'headers']
)
else:
with patch('requests.post') as mock_post:
mock_post.return_value = get_response(http_response, http_headers)
yield
assert_called_with_fields(
mock_post,
http_request,
['params', 'headers', 'json']
)
@contextmanager
def mock_request_exception():
# type: () -> Any
def assert_mock_called(mock_result):
# type: (Any) -> None
mock_result.assert_called()
with patch('requests.get') as mock_get:
mock_get.return_value = True
mock_get.side_effect = requests.exceptions.RequestException
yield
assert_mock_called(mock_get)