I have a python function that calls out to an API using the request function. I want to test a 200 path, and then test a 500 error. I can't seem to figure out how to do it when looking at the requests-mock documentation.
Here is what I want to test.
def get_sku_data(sku: str):
"""
Retrieve a single product from api
"""
uri = app.config["SKU_URI"]
url = f"{uri}/{sku}"
headers = {
"content-type": "application/json",
"cache-control": "no-cache",
"accept": "application/json",
}
try:
retry_times = 3
response = retry_session(retries=retry_times).get(url, headers=headers)
return response.json()
except ConnectionError as ex:
raise Exception(f"Could not connect to sku api at {uri}. {ex}.")
except requests.exceptions.RetryError:
raise Exception(f"Attempted to connect to {uri} {retry_times} times.")
def retry_session(
retries, backoff_factor=0.3, status_forcelist=(500, 502, 503, 504)
) -> requests.Session:
"""
Performs a retry
"""
session = requests.Session()
retry = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
Here is my test stub that I'm trying to get going
# These test use pytest https://pytest.readthedocs.io/en/latest/
# Note: client is a pytest fixture that is dependency injected from src/tests/conftest.py
import json
import pytest
from src.app import create_app
import requests
import requests_mock
#pytest.fixture
def app():
app = create_app()
return app
def test():
session = requests.Session()
adapter = requests_mock.Adapter()
session.mount("mock", adapter)
adapter.register_uri("GET", "mock://12", text="data")
resp = session.get("mock://12")
assert resp.status_code == 200
assert resp.text == "data"
I'm a bit new to python testing, so any help would be very much appreciated.
My perception of how it worked was wrong.
I register the URI I'm testing against and instead of calling session.get, I actually call the function under test.
Related
I am trying to mock the response of api call with pytest in using monkeypatch but without success.
In a file functions.py, I have a function call an external API to get data in json format and I want to test this function
def api_call(url, token):
try:
headers = {"Authorization": "Bearer %s" % token['accessToken']}
session = requests.Session()
response = session.get(url, headers=headers)
json_data = response.json()
return json_data
except Exception as err:
print(f'Other error occurred: {err}')
My test function in file test_functions.py :
from lib import requests
import functions as extfunction
class MockResponse:
def __init__(self, json_data):
self.json_data = json_data
def json(self):
return self.json_data
def test_api_call_get(monkeypatch):
fake_token = {'accessToken' : 'djksjdskjdsjdsljdsmqqqq'}
def mock_get(*args, **kwargs):
return MockResponse({'results': 'test', 'total_sum' : 2000})
monkeypatch.setattr(requests, 'get', mock_get)
# extfunction.api_call, which contains requests.get, uses the monkeypatch
fake_url = 'https://api-test/v2'
response = extfunction.api_call(fake_url, fake_token)
assert response['results'] == 'test'
assert response['total_sum'] == 2000
During test execution, my function api_call (using requests with Get Method) is not mocked and I have an error because function is really called with fake parameters (such as fake_token)
How can I do to fake my response ?
Thanks
You should use the responses library, which is made for this purpose.
A code snippet would look like:
import functions as extfunction
import responses
#responses.activate
def test_api_call_get():
fake_token = {'accessToken' : 'djksjdskjdsjdsljdsmqqqq'}
responses.add(responses.GET, 'https://api-test/v2',
json={'results': 'test', 'total_sum' : 2000})
# extfunction.api_call, which contains requests.get, uses the monkeypatch
fake_url = 'https://api-test/v2'
response = extfunction.api_call(fake_url, fake_token)
assert response['results'] == 'test'
assert response['total_sum'] == 2000
The monkeypatch is modifying the requests function get(), as in
response = requests.get(url, headers=headers)
When you are using requests.Session().get()
This can be monkeypatched by modifying the Session class instead of the requests module.
monkeypatch.setattr(requests.Session, 'get', mock_get)
I am trying to port the "Prepared Request" example from this link: https://docs.python-requests.org/en/latest/user/advanced/ using httpx AsnycClient.
from requests import Request, Session
s = Session()
req = Request('GET', url, data=data, headers=headers)
prepped = s.prepare_request(req)
# do something with prepped.body
prepped.body = 'Seriously, send exactly these bytes.'
# do something with prepped.headers
prepped.headers['Keep-Dead'] = 'parrot'
resp = s.send(prepped,
stream=stream,
verify=verify,
proxies=proxies,
cert=cert,
timeout=timeout
)
print(resp.status_code)
But I could not find any method to prepare the request in the httpx async lib.
In particular, it seems that "prepare_request" method is not implemented.
Can anyone tell me how do I prepare requests using AsyncClient?
Any hint is much appreciated.
One could use httpx.Request class to write an prepare_request factory.
import httpx
def prepare_request(method, url, **kwargs):
# a very simple factory
return httpx.Request(method, url, **kwargs)
request = prepare_request("GET", "https://www.google.com")
client = httpx.Client()
response = client.send(request)
print(response, response.num_bytes_downloaded )
used sync client for demonstration only
I want to use have fault-tolerance for requests.get(url) with a maximum number of 3 retries.
Currently, I create a new session and pass that around between methods, which I would like to avoid:
with requests.Session() as rs:
rs.mount('https://', HTTPAdapter(max_retries=3))
rs.get(url)
...
Is there any way to configure the requests.get(url) call, such that it retries requests to the server in case it fails?
This might be a dirty implementation:
import requests
from time import sleep
While True:
try:
#requests here
break
except:
sleep(1)
But setting a retry of 3:
import requests
from time import sleep
for i in range(3):
try:
#requests here
break
except:
sleep(1)
A good practice:
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def requests_retry_session(
retries=3,
backoff_factor=0.3,
status_forcelist=(500, 502, 504),
session=None,
):
session = session or requests.Session()
retry = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
response = requests_retry_session().get('https://www.example.com/')
print(response.status_code)
s = requests.Session()
s.auth = ('user', 'pass')
s.headers.update({'x-test': 'true'})
response = requests_retry_session(session=s).get(
'https://www.example.com'
)
Each retry attempt will create a new Retry object with updated values, so they can be safely reused.
Documentation is Here.
Note that:
Patching the HTTPAdapter.init() defaults helps you (very much not
recommended).
You can override HTTPAdapter.__init__ with one that sets a default value for the max_retries argument with functools.partialmethod, so that the desired retry behavior is applied globally:
from functools import partialmethod
requests.adapters.HTTPAdapter.__init__ = partialmethod(
requests.adapters.HTTPAdapter.__init__, max_retries=3)
...
requests.get(url)
How do you use Bitbucket's 2.0 API to decline a pull request via Python?
According to their documentaion, it should be something like:
import requests
kwargs = {
'username': MY_BITBUCKET_ACCOUNT,
'repo_slug': MY_BITBUCKET_REPO,
'pull_request_id': pull_request_id
}
url = 'https://api.bitbucket.org/2.0/repositories/{username}/{repo_slug}/pullrequests/{pull_request_id}/decline'.format(**kwargs)
headers = {'Content-Type': 'application/json'}
response = requests.post(url, auth=(USERNAME, PASSWORD), headers=headers)
However, this fails with response.text simply saying "Bad Request".
This similar code works for me with their other API endpoints, so I'm not sure why the decline method is failing.
What am I doing wrong?
You have to authenticate with Oath. I wrote a wrapper for making these requests. Here is a simple example that works. The only thing I couldn't figure out was how to add a reason it was declined. I ended up making a request before I declined the PR that added a comment on why it was declined.
import os
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session
class Bitbucket(object):
def __init__(self, client_id, client_secret, workplace, repo_slug):
self.workplace = workplace # username or company username
self.repo_slug = repo_slug
self.token_url = 'https://bitbucket.org/site/oauth2/access_token'
self.api_url = 'https://api.bitbucket.org/2.0/'
self.max_pages = 10
self.client = BackendApplicationClient(client_id=client_id)
self.oauth = OAuth2Session(client=self.client)
self.oauth.fetch_token(
token_url=self.token_url,
client_id=client_id,
client_secret=client_secret
)
def get_api_url(self, endpoint):
return f'{self.api_url}repositories/{self.workplace}/{self.repo_slug}/{endpoint}'
bitbucket = Bitbucket(os.environ['BITBUCKET_KEY'], os.environ['BITBUCKET_SECRET'], workplace='foo', repo_slug='bar')
pr_id = 1234
resp = bitbucket.oauth.post(f"{bitbucket.get_api_url('pullrequests')}/{pr_id}/decline")
if resp.status_code == 200:
print('Declined')
else:
print('Someting went wrong.')
I'm testing for a specific response code and want to mock out a test case when the code is something different, like unauthorized 401. I'm using the Python 3.7 http.client library and pytest
So far I tried to use the #patch decorator and call a function with side_effect to trigger the exception
my test case:
from unittest import mock
from application import shorten_url
def mock_status(url):
raise ConnectionError
#patch("application.shorten_url", side_effect=mock_status)
def test_bitly(client):
with pytest.raises(ConnectionError) as e:
shorten_url("something")
my code:
def shorten_url(url):
conn = http.client.HTTPSConnection("api-ssl.bitly.com", timeout=2)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer abcd",
}
payload = json.dumps({"long_url": url})
conn.request("POST", "/v4/shorten", payload, headers)
res = conn.getresponse()
if not res.status == 201:
raise ConnectionError
data = json.loads(res.read())
return data["link"]
I don't really understand how to raise this exception correctly using mock and side_effect.
A friend helped me with the issue, this seems to work (it's still very confusing for me):
from unittest.mock import patch, MagicMock
#patch("http.client.HTTPSConnection")
#patch("http.client.HTTPResponse")
def test_bitly(mock_conn, mock_res):
mock_res.status = 400
mock_conn.getresponse = MagicMock(return_value=mock_res)
with pytest.raises(ConnectionError):
shorten_url("fake-url")
I think this answer is easier to understand. First I created a fake http connection and response:
class FakeHTTPConnection:
def __init__(self, status):
self.status = status
def request(self, *args):
# If you need to do any logic to change what is returned, you can do it in this class
pass
def getresponse(self):
return FakeHTTPResponse(self.status)
class FakeHTTPResponse:
def __init__(self, status):
self.status = status
Then in my test class, I overrode http.client.HTTPConnection to create my instance instead.
class TestFoo(unittest.TestCase):
#patch('http.client.HTTPConnection', new=MagicMock(return_value=FakeHTTPConnection(200)))
def test_foo_success(self):
conn = http.client.HTTPConnection("127.0.0.1")
conn.request("GET", "/endpoint")
response = conn.getresponse()
success = response.status == http.HTTPStatus.OK