Conditionally mock httputil.get method on specific url with path json - python

I have http get method mocked so to get the response from the url without actually sending the url:
def get(url, retries=None, back_off_factor=None, max_back_off=None, timeout=None, response_encoding=None,
retry_on_timeout=None, retry_codes=None, **kwargs):
return _make_request("GET", url,
retries=retries, back_off_factor=back_off_factor,
max_back_off=max_back_off,
timeout=timeout,
response_encoding=response_encoding,
retry_on_timeout=retry_on_timeout,
retry_codes=retry_codes,
**kwargs)
#patch('lib.httputil.get')
def test_harvest(self, mock_get):
articles = json.load(json_file)
# Configure the mock to return a response with an OK status code. Also, the mock should have
# a `json()` method that returns a list of todos.
mock_get.return_value = Mock(ok=True)
mock_get.return_value.json.return_value = articles
mock_get.return_value.status_code = 200
the_rest_of_the_test()
But I realized I need to mock it only if the URL is specific. I know I could use new keyword and do:
def mock_get(self, url):
if url == MY_SPECIFIC_URL:
{...}
else:
self.old_get(url)
{...}
with mock.patch('portality.lib.httputil.get', new=self.mock_get):
the_rest_of_the_test()
but I don't really know how to mock the Response object so that it returns the correct status code and gives the correct result to .json() method.
How can I use both of these approaches altogether so that on one hand I can use the conditional but on the other mock the Response in easy way?

I suggest that you use the requests library, along with responses which is specifically meant for returning the desired HTTP responses.
You can mock specific urls:
import responses
import requests
#responses.activate
def test_simple():
responses.add(responses.GET, 'http://twitter.com/api/1/foobar',
json={'error': 'not found'}, status=404)
resp = requests.get('http://twitter.com/api/1/foobar')
assert resp.json() == {"error": "not found"}
assert len(responses.calls) == 1
assert responses.calls[0].request.url == 'http://twitter.com/api/1/foobar'
assert responses.calls[0].response.text == '{"error": "not found"}'
And you can exclude other urls:
responses.add_passthru(re.compile('https://percy.io/\\w+'))

Related

Unit Testing Replace remote API Server with predefined response

So, I have a server running FastAPI which will make a API call to a remote API upon request.
I am developping unit-testing for this application, but here comes the question:
Can I, for the purpose of the test, replace a legit remote API server response by a predefined response ?
Example of the tests runned:
from fastapi.testclient import TestClient
from web_api import app
client = TestClient(app)
def test_get_root():
response = client.get('/')
assert response.status_code == 200
assert response.json() == {"running": True}
And the my server
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def home():
return {"running": True}
This is a simple example, but on other endpoints of my API I would call an external remote API
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
Because I want to test the response of MY API, I would like to replace the remote API with a predefined response.
Also, one user request can end-up in multiple background API requests with transformed pieces of data.
Edit
Here are some more details on the structure of the application:
#app.get("/stuff/.......",
# lots of params
)
def get_stuff_from_things(stuff:list, params):
api = API(api_key=...)
# Do some stuff with the params
things = generate_things_list(params)
api.search_things(params)
# Check the result
# do some other stuff
return some_response
class API:
BASE_URL = 'https://api.example.com/'
def search_things(self, params):
# Do some stuff
# like putting stuff in the params
for s in stuff:
s.update(self.get_thing(params)) # -> get_thing()
# Do some more stuff
return stuff
# get_thing <- search_things
def get_thing(self, params...):
# Some stuff
results = self.call_api('something', params) # -> call_api()
json = results.json()
# Some more stuff
things = []
for thing in json['things']:
t = Thing(thing)
things.append(t)
return things
# call_api <- get_thing
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
self.last_response = response
return response
Nb. That is pseudo-code, I simplified the functions by removing the parameters, etc.
I hope it is clear, thanks for your help.
A complex API method might look like this (please pay attention to the depends mechanism - it is crucial):
import urllib
import requests
from fastapi import FastAPI, Depends
app = FastAPI()
# this can be in a different file
class RemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
url = baseurl + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
#app.get("/complex_api")
def calls_other_api(remote_call_wrapper=Depends(RemoteCallWrapper)):
response = remote_call_wrapper.call_api("https://jsonplaceholder.typicode.com",
"/todos/1", None)
return {"result": response.json()}
Now, we wish to replace the remote call class. I wrote a helper library that simplifies the replacement for tests - pytest-fastapi-deps:
from fastapi.testclient import TestClient
from mock.mock import Mock
from requests import Response
from web_api import app, RemoteCallWrapper
client = TestClient(app)
class MyRemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
the_response = Mock(spec=Response)
the_response.json.return_value = {"my": "response"}
return the_response
def test_get_root(fastapi_dep):
with fastapi_dep(app).override({RemoteCallWrapper: MyRemoteCallWrapper}):
response = client.get('/complex_api')
assert response.status_code == 200
assert response.json() == {"result": {"my": "response"}}
You override the RemoteCallWrapper with your MyRemoteCallWrapper implementation for the test, which has the same spec.
As asserted - the response changed to our predefined response.
It sounds like you'd want to mock your call_api() function.
With a small modification to call_api() (returning the result of .json()), you can easily mock the whole function while calling the endpoint in your tests.
I'll use two files, app.py and test_app.py, to demonstrate how I would do this:
# app.py
import requests
import urllib
from fastapi import FastAPI
app = FastAPI()
def call_api(self, endpoint: str, params: dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response.json() # <-- This is the only change. Makes it easier to test things.
#app.get("/")
def home():
return {"running": True}
#app.get("/call-api")
def make_call_to_external_api():
# `endpoint` and `params` could be anything here and could be different
# depending on the query parameters when calling this endpoint.
response = call_api(endpoint="something", params={})
# Do something with the response...
result = response["some_parameter"]
return result
# test_app.py
from unittest import mock
from fastapi import status
from fastapi.testclient import TestClient
import app as app_module
from app import app
def test_call_api_endpoint():
test_response = {
"some_parameter": "some_value",
"another_parameter": "another_value",
}
# The line below will "replace" the result of `call_api()` with whatever
# is given in `return_value`. The original function is never executed.
with mock.patch.object(app_module, "call_api", return_value=test_response) as mock_call:
with TestClient(app) as client:
res = client.get("/call-api")
assert res.status_code == status.HTTP_200_OK
assert res.json() == "some_value"
# Make sure the function has been called with the right parameters.
# This could be dynamic based on how the endpoint has been called.
mock_call.assert_called_once_with(endpoint="something", params={})
If app.py and test_app.py are in the same directory you can run the tests simply by running pytest inside that directory.

Query params for nested objects with python requests module like QS.stringify

When creating a Strapi client in Python using the requests module I came across a problem.
When using the params argument for the requests.get method with a nested dictionary it doesn't translate to a querystring like the npm package qs does.
I wanted a qs.stringify in python but found the answer nowhere on Stack Overflow.
I found this package:
https://github.com/cine-io/nested-query-string
def _get(self, path, params=None, **kwargs):
url = self.__construct_url(path)
headers = self.__get_request_headers()
if params is not None:
#see line here!!
from nested_query_string import NestedQueryString
params = NestedQueryString.encode(params)
response = requests.get(url, params=params, headers=headers, **kwargs)
assert response.status_code == 200
return self.__parse_strapi_response(response)

Why does my mocking of an URL with requests not work?

I tried to mock a specific URL as shown in this example:
How can I mock requests and the response?
to test my own function:
class URLStatus():
#staticmethod
def check(url, redirects):
try:
session = requests.Session()
session.max_redirects = redirects
urlcheck = session.get(url)
return urlcheck.status_code
The issue is that it never takes the mocked url, but instead only takes real ones.
import requests
from unittest import TestCase, mock
from unittest.mock import patch
from lib.checks.url_status import URLStatus
def mocked_requests_get(*args, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
if args[0] == 'http://someurl.com/test.json':
return MockResponse({"key1": "value1"}, 200)
elif args[0] == 'http://someotherurl.com/anothertest.json':
return MockResponse({"key2": "value2"}, 200)
return MockResponse(None, 404)
class URLStatusTestCase(TestCase):
#mock.patch('lib.checks.url_status.requests.get', side_effect=mocked_requests_get)
def test_check(self, mock_get):
url_status = URLStatus()
test_data = url_status.check('http://someurl.com/test.json', 5)
self.assertEqual(test_data, 200)
if __name__ == '__main__':
unittest.main()
This one, for example, fails because it sees 'http://someurl.com/test.json' as a 404, not a 200. I have no idea why this is happening.
How do I make it take the mocked URL?
You are mocking the wrong function. requests.get is a convenience function that creates its own, one-use Session, then uses its get method to provide the result. Your check method is using its own Session object; you need to at least mock that object's get method.
Given that you aren't reusing this session elsewhere, it would probably be simplest to change its implementation to take advantage of requests.get:
class URLStatus():
#staticmethod
def check(url, redirects):
return requests.get(url, max_redirects=redirects).status_code

python Mock post inside a method

How can I mock a post inside a method, so i can have unittests?
def send_report(self, data):
url = settings.WEBHOOK_PO
payload = json.dumps(data)
requests.post(url, data=payload)
url = settings.WEBHOOK_LQA
response = requests.post(url, data=payload)
return response.status_code
Is there a way to cover this method for unit test with not actually posting?
You can use the mock library to replace requests.post with something else:
with mock.patch('requests.post') as mock_post:
foo.send_report(data)
(mock is a third-party package, but was added to the standard library, as part of the unittest package`, in Python 3.3.)
mock_post can be configured to provide the desired behavior during the test; consult the mock documentation for details.
Another option is to modify your method to take the post function as an argument, rather than hard-coding the function (this is an example of dependency injection):
def send_report(self, data, poster=requests.post):
url = settings.WEBHOOK_PO
payload = json.dumps(data)
poster(url, data=payload)
url = settings.WEBHOOK_LQA
response = poster(url, data=payload)
return response.status_code
When you want to test the function, you simply pass a different callable object as the optional second argument.
Note that you can supply separate functions for the two types of posts, which might make it easier to test than with a mock:
from functools import partial
def send_report(self,
data,
post_po=partial(requests.post, settings.WEBHOOK_PO),
post_lqa=partial(requests.post, settings.WEBHOOK_LQA)):
payload = json.dumps(data)
post_po(data=payload)
response = post_lqa(data=payload)
return response.status_code

how to test the result of an API without requesting it

I've got a function like:
def request_API(request_url): #To test
fail_request = -1
response = requests.get(request_url)
if response.ok:
infos = json.loads(response.text)
if infos.has_key("movie"):
return infos["movie"]
if infos.has_key("tvseries"):
return infos["tvseries"]
print "Allocine API Request Error"
return fail_request
I did a test like:
def test_should_fail_to_request(self):
#GIVEN
response = json.dumps({"error":{"code":0,"$":"No result"}})
requests.get = mock.MagicMock(return_value=response)
#WHEN
response = my_mod.request_allocine_API("") #requests mocked so we don't need an URL
#THEN
self.assertEqual(response, -1, "There should be an error in the API")
But I've got an error:
AttributeError: 'str' object has no attribute 'ok'
I know it come from the fact that when I mock request.get I return a JSON. My question is what is the proper way to do it. Have I to recreate an object requests or is there more simple way to do so.
You are mocking requests.get, which normally returns an Response object, to instead return a plain string. Try having it return an Response object instead:
from mock import patch
from requests import Response
def test_should_fail_to_request(self):
mock_response = Response()
mock_response.status_code = 404
mock_response.body = json.dumps({"error":{"code":0,"$":"No result"}})
with patch('requests.get', return_value=mock_response):
response = my_mod.request_allocine_API("")
self.assertEqual(response, -1, "There should be an error in the API")
I use requests-mock library which works well.
the document is in : https://requests-mock.readthedocs.org/en/latest/
The best feature is supporting for regex.

Categories

Resources