I'm trying to mock external API calls in the pyonfleet package. My desired usage is something like below.
def get_task(id):
from onfleet import Onfleet
api = Onfleet()
return api.tasks.get(id=id)
with patch('onleet.Onfleet.tasks.get', return_value={}):
task = get_task(id='...')
The problem is the tasks attribute and it's API endpoints are set on the Onfleet object using setattr in its _initialize_resources method. Because of this all my tests are raising the error:
AttributeError: type object 'Onfleet' has no attribute 'tasks'
class Onfleet(object):
# Loading config data
data = Config.data
# Look up local authentication JSON if no api_key was passed
if (os.path.isfile(".auth.json")):
with open(".auth.json") as json_secret_file:
secret = json.load(json_secret_file)
def __init__(self, api_key=None):
self._session = requests.Session()
# auth takes api_key and api_secret
self._session.auth = (api_key if api_key else self.secret["API_KEY"], "")
self._initialize_resources(self._session)
def auth_test(self):
path = self.data["URL"]["base_url"] + self.data["URL"]["version"] + self.data["URL"]["auth_test"]
response = self._session.get(path)
return response.json()
def _initialize_resources(self, session):
resources = self.data["RESOURCES"]
# Go through the config module to create endpoints
for endpoint, http_methods in resources.items():
setattr(self, endpoint, Endpoint(http_methods, session))
How do I patch methods on an object that are added dynamically?
Related
I am working on writing unittest for my fastapi project.
One endpoint includes getting a serviceNow ticket. Here is the code i want to test:
from aiosnow.models.table.declared import IncidentModel as Incident
from fastapi import APIRouter
router = APIRouter()
#router.post("/get_ticket")
async def snow_get_ticket(req: DialogflowRequest):
"""Retrieves the status of the ticket in the parameter."""
client = create_snow_client(
SNOW_TEST_CONFIG.servicenow_url, SNOW_TEST_CONFIG.user, SNOW_TEST_CONFIG.pwd
)
params: dict = req.sessionInfo["parameters"]
ticket_num = params["ticket_num"]
try:
async with Incident(client, table_name="incident") as incident:
response = await incident.get_one(Incident.number == ticket_num)
stage_value = response.data["state"].value
desc = response.data["description"]
[...data manipulation, unimportant parts]
What i am having trouble with is trying to mock the client response, every time the actual client gets invoked and it makes the API call which i dont want.
Here is the current version of my unittest:
from fastapi.testclient import TestClient
client = TestClient(app)
#patch("aiosnow.models.table.declared.IncidentModel")
def test_get_ticket_endpoint_valid_ticket_num(self, mock_client):
mock_client.return_value = {"data" : {"state": "new",
"description": "test"}}
response = client.post(
"/snow/get_ticket", json=json.load(self.test_request)
)
assert response.status_code == 200
I think my problem is patching the wrong object, but i am not sure what else to patch.
In your test your calling client.post(...) if you don't want this to go to the Service Now API this client should be mocked.
Edit 1:
Okay so the way your test is setup now the self arg is the mocked IncidentModel object. So only this object will be a mock. Since you are creating a brand new IncidentModel object in your post method it is a real IncidentModel object, hence why its actually calling the api.
In order to mock the IncidentModel.get_one method so that it will return your mock value any time an object calls it you want to do something like this:
def test_get_ticket_endpoint_valid_ticket_num(mock_client):
mock_client.return_value = {"data" : {"state": "new",
"description": "test"}}
with patch.object(aiosnow.models.table.declared.IncidentModel, "get_one", return_value=mock_client):
response = client.post(
"/snow/get_ticket", json=json.load(self.test_request)
)
assert response.status_code == 200
The way variable assignment works in python, changing aiosnow.models.table.declared.IncidentModel will not change the IncidentModel that you've imported into your python file. You have to do the mocking where you use the object.
So instead of #patch("aiosnow.models.table.declared.IncidentModel"), you want to do #patch("your_python_file.IncidentModel")
From the docs at https://docs.python-requests.org/en/master/user/authentication/
I gathered that the __call__ function in my own Auth Class should have the r argument,
However when i go to call this class in requests.get(auth=MyClass), I get the error TypeError: __call__() missing 1 required positional argument: 'r'
The code for my class can be found here https://pastebin.com/YDZ2DeaT
import requests
import time
import base64
from requests.auth import AuthBase
class TokenAuth(AuthBase):
"""Refreshes SkyKick token, for use with all Skykick requests"""
def __init__(self, Username: str, SubKey: str):
self.Username = Username
self.SubKey = SubKey
# Initialise with no token and instant expiry
self.Token = None
self.TokenExpiry = time.time()
self.Headers = {
# Request headers
'Content-Type' : 'application/x-www-form-urlencoded',
'Ocp-Apim-Subscription-Key': self.SubKey,
}
self.Body = {
# Request body
'grant_type': 'client_credentials',
'scope' : 'Partner'
}
def regenToken(self):
# Sends request to regenerate token
try:
# Get key from API
response = requests.post("https://apis.skykick.com/auth/token",
headers=self.Headers,
auth=(self.Username, self.SubKey),
data=self.Body,
).json()
except:
raise Exception("Sending request failed, check connection.")
# API errors are inconsistent, easiest way to catch them
if "error" in response or "statusCode" in response:
raise Exception(
"Token requesting failed, cannot proceed with any Skykick actions, exiting.\n"
f"Error raised was {response}")
# Get token from response and set expiry
self.Token = response["access_token"]
self.TokenExpiry = time.time() + 82800
def __call__(self, r):
# If token expiry is now or in past, call regenToken
if self.TokenExpiry <= time.time():
self.regenToken()
# Set headers and return complete requests.Request object
r.headers["Authorization"] = f"Bearer {self.Token}"
return r
# Initialise our token class, so it is ready to call
TokenClass = TokenAuth("test", "1234")
#Send request with class as auth method.
requests.get("https://apis.skykick.com/whoami", auth=TokenClass())
I've tried using the example code, which works, but I can't figure out why mine won't work.
python-requests version is 2.25.1
I think I know what is going on.
This line instantiates an object, called TokenClass
TokenClass = TokenAuth("test", "1234")
then here,
requests.get("https://apis.skykick.com/whoami", auth=TokenClass())
you are calling that object like a function
when you call an object like a function, python looks for the __call__ method of the object.
And you are not calling in any arguments here. What you have is roughly the same as this I think
requests.get("https://apis.skykick.com/whoami", auth=TokenClass.__call__())
and so it complains that you are missing the r argument
This is their example:
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
requests.get(url, auth=MyAuth())
MyAuth is a class that they define, and then MyAuth() creates an instance of it that they pass in to get.
Yours is more like this
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
myAuth = MyAuth() # create an instance of the class
requests.get(url, auth=myAuth()) # call the instance and pass in result
It could also be written like this
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
requests.get(url, auth=MyAuth()())
This program with produce the same error you are getting
import requests
class MyAuth(requests.auth.AuthBase):
def __call__(self, r):
# Implement my authentication
return r
url = 'https://httpbin.org/get'
MyAuth()()
because when you put () after a class, you get an instance, and when you put () after an instance, you call the __call__ method
How does one keep the attributes of an instance of a class up-to-date if the are changing moment to moment?
For example, I have defined a class describing my stock trading brokerage account balances. I have defined a function which pings the brokerage API and returns a JSON object with the current status of various parameters. The status of these parameters are then set as attributes of a given instance.
import json
import requests
from ConfigParser import SafeConfigParser
class Account_Balances:
def Account_Balances_Update():
"""Pings brokerage for current status of target account"""
#set query args
endpoint = parser.get('endpoint', 'brokerage') + 'user/balances'
headers = {'Authorization': parser.get('account', 'Auth'), 'Accept': parser.get('message_format', 'accept_format')}
#send query
r = requests.get(endpoint, headers = headers)
response = json.loads(r.text)
return response
def __init__(self):
self.response = self.Account_Balances_Update()
self.parameterA = response['balances']['parameterA']
self.parameterB = response['balances']['parameterB']
As it stands, this code sets the parameters at the moment the instance is created but they become static.
Presumably parameterA and parameterB are changing moment to moment so I need to keep them up-to-date for any given instance when requested. Updating the parameters requires rerunning the Account_Balances_Update() function.
What is the pythonic way to keep the attribute of a given instance of a class up to date in a fast moving environment like stock trading?
Why not just creating an update method?
class Account_Balances:
#staticmethod
def fetch():
"""Pings brokerage for current status of target account"""
#set query args
endpoint = parser.get('endpoint', 'brokerage') + 'user/balances'
headers = {'Authorization': parser.get('account', 'Auth'), 'Accept': parser.get('message_format', 'accept_format')}
#send query
r = requests.get(endpoint, headers = headers)
response = json.loads(r.text)
balances = response['balances']
return balances['parameterA'], balances['parameterB']
def update(self):
self.parameterA, self.parameterB = self.fetch()
This is the method in ReportRunner class in report_runner.py in my Flask-Restful app:
class ReportRunner(object):
def __init__(self):
pass
def setup_routes(self, app):
app.add_url_rule("/run_report", view_func=self.run_report)
def request_report(self, key):
# code #
def key_exists(self, key):
# code #
def run_report(self):
key = request.args.get("key", "")
if self.key_exists(key):
self.request_report(report_type, key)
return jsonify(message = "Success! Your report has been created.")
else:
response = jsonify({"message": "Error => report key not found on server."})
response.status_code = 404
return response
and the nose test calls the URL associated with that route
def setUp(self):
self.setup_flask()
self.controller = Controller()
self.report_runner = ReportRunner()
self.setup_route(self.report_runner)
def test_run_report(self):
rr = Report(key = "daily_report")
rr.save()
self.controller.override(self.report_runner, "request_report")
self.controller.expectAndReturn(self.report_runner.request_report("daily_report"), True )
self.controller.replay()
response = self.client.get("/run_report?key=daily_report")
assert_equals({"message": "Success! Your report has been created."}, response.json)
assert_equals(200, response.status_code)
and the test was failing with the following message:
AttributeError: 'Response' object has no attribute 'json'
but according to the docs it seems that this is how you do it. Do I change the return value from the method, or do I need to structure the test differently?
The test is now passing written like this:
json_response = json.loads(response.data)
assert_equals("Success! Your report has been created.", json_response["message"])
but I'm not clear on the difference between the two approaches.
According to Flask API Response object doesn't have attribute json (it's Request object that has it). So, that's why you get exception. Instead, it has generic method get_data() that returns the string representation of response body.
json_response = json.loads(response.get_data())
assert_equals("Success! Your report has been created.", json_response.get("message", "<no message>"))
So, it's close to what you have except:
get_data() is suggested instead of data as API says: This should not be used and will eventually get deprecated.
reading value from dictionary with get() to not generate exception if key is missing but get correct assert about missing message.
Check this Q&A also.
In my python code I have global requests.session instance:
import requests
session = requests.session()
How can I mock it with Mock? Is there any decorator for this kind of operations? I tried following:
session.get = mock.Mock(side_effect=self.side_effects)
but (as expected) this code doesn't return session.get to original state after each test, like #mock.patch decorator do.
Since requests.session() returns an instance of the Session class, it is also possible to use patch.object()
from requests import Session
from unittest.mock import patch
#patch.object(Session, 'get')
def test_foo(mock_get):
mock_get.return_value = 'bar'
Use mock.patch to patch session in your module. Here you go, a complete working example https://gist.github.com/k-bx/5861641
With some inspiration from the previous answer and :
mock-attributes-in-python-mock
I was able to mock a session defined like this:
class MyClient(object):
"""
"""
def __init__(self):
self.session = requests.session()
with that: (the call to get returns a response with a status_code attribute set to 200)
def test_login_session():
with mock.patch('path.to.requests.session') as patched_session:
# instantiate service: Arrange
test_client = MyClient()
type(patched_session().get.return_value).status_code = mock.PropertyMock(return_value=200)
# Act (+assert)
resp = test_client.login_cookie()
# Assert
assert resp is None
I discovered the requests_mock library. It saved me a lot of bother. With pytest...
def test_success(self, requests_mock):
"""They give us a token.
"""
requests_mock.get("https://example.com/api/v1/login",
text=(
'{"result":1001, "errMsg":null,'
f'"token":"TEST_TOKEN",' '"expire":1799}'))
auth_token = the_module_I_am_testing.BearerAuth('test_apikey')
assert auth_token == 'TEST_TOKEN'
The module I am testing has my BearerAuth class which hits an endpoint for a token to start a requests.session with.