I have the following function and it is a generic function which will make API call based on the input hostname and data. It will construct http request to make API and will return the response. This function will throw four types of exception(invalid URL, timeout, auth error and status check). How can I Mcok and Test the exception raised in API call using pytest? Which will be the best method to test the exceptions raised from API call?
import ssl
import urllib
import urllib.request
import urllib.error
import xml
import xml.etree.ElementTree as ET
def call_api(hostname, data):
'''Function to make API call
'''
# Todo:
# Context to separate function?
# check response for status codes and return reponse.read() if success
# Else throw exception and catch it in calling function
error_codes = {
"1": "Unknown command",
"6": "Bad Xpath",
"7": "Object not present",
"8": "Object not unique"
}
url = "http://" + hostname + "/api"
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
try:
response = urllib.request.urlopen(url, data=encoded_data,
timeout=10).read()
root = ET.fromstring(response)
if root.attrib.get('status') != "success":
Errorcode = root.attrib.get('code')
raise Exception(pan_error_codes.get(Errorcode, "UnknownError"),
response)
else:
return response
except urllib.error.HTTPError as e:
raise Exception(f"HttpError: {e.code} {e.reason} at {e.url}", None)
except urllib.error.URLError as e:
raise Exception(f"Urlerror: {e.reason}", None)
If i call this function
def create_key(hostname, username, password):
hostname = 'myhost ip'
data = {
'type': 'keygen',
'username': username,
'password': password
}
username = 'myuser'
password = 'password'
response = call_api(hostname, data)
return response
i will get a response like following
b"<response status = 'success'><result><key>mykey</key></result></response>"
You can mock error raising via side_effect parameter:
Alternatively side_effect can be an exception class or instance. In this case the exception will be raised when the mock is called.
In your case, this can be used like this (assuming call_api is defined in module foo):
import pytest
from unittest.mock import patch
def test_api():
with patch('foo.call_api', side_effect=Exception('mocked error')):
with pytest.raises(Exception) as excinfo:
create_key('localhost:8080', 'spam', 'eggs')
assert excinfo.value.message == 'mocked error'
Related
Python version 3.9, FastAPI version 0.78.0
I have a custom function that I use for application exception handling. When requests run into internal logic problems, i.e I want to send an HTTP response of 400 for some reason, I call a utility function.
#staticmethod
def raise_error(error: str, code: int) -> None:
logger.error(error)
raise HTTPException(status_code=code, detail=error)
Not a fan of this approach. So I look at
from fastapi import FastAPI, HTTPException, status
from fastapi.respones import JSONResponse
class ExceptionCustom(HTTPException):
pass
def exception_404_handler(request: Request, exc: HTTPException):
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"message": "404"})
app.add_exception_handler(ExceptionCustom, exception_404_handler)
The problem I run into with the above approach is the inability to pass in the message as an argument.
Any thoughts on the whole topic?
Your custom exception can have any custom attributes that you want. Let's say you write it this way:
class ExceptionCustom(HTTPException):
pass
in your custom handler, you can do something like
def exception_404_handler(request: Request, exc: HTTPException):
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND, content={"message": exc.detail})
Then, all you need to do is to raise the exception this way:
raise ExceptionCustom(status_code=404, detail='error message')
Note that you are creating a handler for this specific ExceptionCustom. If all you need is the message, you can write something more generic:
class MyHTTPException(HTTPException):
pass
def my_http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(status_code=exc.status_code, content={"message": exc.detail})
app.add_exception_handler(MyHTTPException, my_http_exception_handler)
This way you can raise any exception, with any status code and any message and have the message in your JSON response.
There's a detailed explanation on FastAPI docs
You can add custom exception handlers, and use attributes in your Exception class (i.e., class MyException(Exception) in the example below) to pass any message/variables that you would like to do so. The exception handler (i.e., #app.exception_handler(MyException) in the case below) will handle the exception as you wish and return your custom message. For more options, please have a look at this related answer as well.
Working Example
To trigger the exception in the example below, access the following URL from your browser: http://localhost:8000/something
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
class MyException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
#app.exception_handler(MyException)
async def my_exception_handler(request: Request, exc: MyException):
return JSONResponse(status_code=status.HTTP_404_NOT_FOUND,
content={"message": f"{exc.name} cannot be found." })
#app.get("/{name}")
def read_name(name: str):
if name == "something":
raise MyException(name=name)
return {"name": name}
I am trying to raise the custom exception using the starlette framework in python. I have the API call which checks some condtions depends on the result, it should raise exception.
I have two files app.py and error.py
#app.py
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from error import EmptyParamError
async def homepage(request):
a=1
b=0
if a == 1:
raise EmptyParamError(400, "status_code")
return JSONResponse({'hello': 'world'})
routes = [
Route("/", endpoint=homepage)
]
app = Starlette(routes=routes,debug=True)`
#error.py ```
from starlette.responses import JSONResponse
class BaseError(Exception):
def __init__(self, status_code: int, detail: str = None) -> None:
if detail is None:
detail = "http.HTTPStatus(status_code).phrase"
self.status_code = status_code
self.detail = detail
async def not_found(self):
return JSONResponse(content=self.title, status_code=self.status_code)
class EmptyParamError(BaseError):
""" Error is raised when group name is not provided. """
status_code = 400
title = "Missing Group Name"
When the condition is true, i want to raise the exception but its not returning the jsonrespnse but its returning the stacktrace on the console.
Please let me know if anything is wrong here
adding try block resolved the issue
try:
if a==1:
raise InvalidUsage(100,"invalid this one")
if b == 0:
raise EmptyParamError("this is empty paramuvi")
except InvalidUsage as e:
return JSONResponse({'hello': str(e.message)})
except EmptyParamError as e:
return JSONResponse({'hello': str(e.message)})
When I automated this I came to know that the 500 error that is thrown is given by server. But at our code level we have handled the exception and the response =null
So when I tried to assert with response.status==500 it fails as the response is altogether null.
package test
import geb.spock.GebReportingSpec
import groovyx.net.http.RESTClient
import net.sf.json.JSON
import org.apache.log4j.Logger
import spock.lang.Specification
import spock.lang.Unroll
class EventsAPITest extends GebReportingSpec{
static Logger logger = Logger.getLogger("EventsAPITest");
#Unroll
def "Call calendar event for Invalid Location id 110 throws groovyx.net.http.HttpResponseException"() {
given:
RESTClient restClient = new RESTClient("https://test-api2.club-os.com")
def response=restClient.get(path: '/authenticate',requestContentType: JSON,query:[
'username': 'apiuser',
'password':'apipassword',
])
def authToken=response.data.token
logger.info"Token:" +authToken
restClient.defaultRequestHeaders['Authorization'] = authToken
def locationId=110
when:
response=restClient.get(path: '/locations/'+locationId+'/events',requestContentType: JSON, query:[
'startTimeStartAt': '2018-05-25T17:00:00.000Z',
'startTimeEndAt':'2018-06-01T17:00:00.000Z',
'eventTypeId' : 2 ])
then:
thrown groovyx.net.http.HttpResponseException
logger.info"Response: "+ response
// assert response.data.errorMessage=="Error"
assert response.status == 500
//assert response.status == 500
// assert response.data.errorMessage == "An unknown error occurred"
}
I would like to be able to assert a 500 error and have that returned.
This is not spock-specific. Your code throws an exception, therefore it cannot return a value.
UPDATE after kriegaex's advice in the comments.
By assigning the result of the thrown groovyx.net.http.HttpResponseException expression to a variable, you can access the exception's state. This exception's API is documented here: http://javadox.com/org.codehaus.groovy.modules.http-builder/http-builder/0.6/groovyx/net/http/HttpResponseException.html.
You can access the statusCode property directly, and you have a response property too. That's everything you need to test your conditions.
This is the resulting code:
import geb.spock.GebReportingSpec
import groovyx.net.http.RESTClient
import net.sf.json.JSON
import org.apache.log4j.Logger
import spock.lang.Specification
import spock.lang.Unroll
class EventsAPITest extends GebReportingSpec{
static Logger logger = Logger.getLogger("EventsAPITest");
#Unroll
def "Call calendar event for Invalid Location id 110 throws groovyx.net.http.HttpResponseException"() {
given:
RESTClient restClient = new RESTClient("https://test-api2.club-os.com")
def response=restClient.get(path: '/authenticate',requestContentType: JSON,query:[
'username': 'apiuser',
'password':'apipassword',
])
def authToken=response.data.token
logger.info"Token:" +authToken
restClient.defaultRequestHeaders['Authorization'] = authToken
def locationId=110
when:
response=restClient.get(path: '/locations/'+locationId+'/events',requestContentType: JSON, query:[
'startTimeStartAt': '2018-05-25T17:00:00.000Z',
'startTimeEndAt':'2018-06-01T17:00:00.000Z',
'eventTypeId' : 2 ])
then:
//Here, assign the exception to a variable.
def e= thrown groovyx.net.http.HttpResponseException
logger.info"Response: "+ e.response
// assert response.data.errorMessage=="Error"
assert e.response.status == 500
//assert response.status == 500
// assert response.data.errorMessage == "An unknown error occurred"
}
I am a junior developer and trying to write some unittests for our API endpoints. Below is the class that i am testing and the actual test that runs without any issue. (But i am still hesitant that it's hitting my methods). My question is how can i improve my test and also make sure it covers exception(in this case ValueError, SystemError, Exception) by using side_effects(or any better suggestions) from python mock? I read python mock documentation but still can't figure out how to improve and importantly test exceptions.
we use flask microframework, python3.x,
--- Class that i am testing:
#USER_MOD.route('', methods=[HttpMethods.HTTP_POST])
#jwt_required
def create_user():
"""
Private end point for creating users of the system
:return: json
"""
response = ""
try:
logger_obj.info("Request : POST : Create User: {} ".format(request.base_url))
# validating input request
if ValidationUtils.validate_request(request, "CREATE_USER"):
logger_obj.debug("Request Validation Successful")
response = UserService().create_new_user(request)
logger_obj.debug("User Created: {} ".format(response))
return jsonify(response), HTTPStatus.OK
except ValueError as error:
logger_obj.error("ValueError create_user() : {}".format(error))
response = ApplicationUtils.create_api_response(status=ResponseConstants.MSG_ERROR, message=str(error))
return jsonify(response), HTTPStatus.BAD_REQUEST
except SystemError as error:
logger_obj.error("SystemError create_user() : {}".format(error))
response = ApplicationUtils.create_api_response(status=ResponseConstants.MSG_ERROR, message=str(error))
return jsonify(response), HTTPStatus.INTERNAL_SERVER_ERROR
except Exception as error:
logger_obj.error("Exception create_user() : {}".format(error))
response = ApplicationUtils.create_api_response(status=ResponseConstants.MSG_ERROR, message=str(error))
return jsonify(response), HTTPStatus.UNAUTHORIZED
finally:
logger_obj.info("Response : POST : Create User: {} : {}".format(request.base_url, response))
--- Test for above class:
class UserApiTests(DataTestCase): //(or i can use unittest.TestCase)
def setUp(self):
os.environ.update(self.config.to_env())
self.flask_app = make_flask_app()
self.client = self.flask_app.test_client()
self.flask_app.testing = True
#patch('usermgmt.app.services.user_service.UserService.create_new_user')
def test_create_user(self, mock_create_new_user):
# Given
mock_create_new_user.return_value.status_code = 200
mock_create_new_user.return_value = {
"status": "SUCCESS"
}
data_to_post = {
"name": "Test2",
"email": "new-user2#entity.com",
"entity_id": 1,
"is_active": True,
"product_roles": [
{"product_id": 1, "role_id": 4},
{"product_id": 2, "role_id": 4}
],
}
# When
response = self.client.post('/api/usermgmt/users', data=json.dumps(data_to_post), headers={
"Authorization": "Bearer {}".format(get_jwt(identity=self), "Content-Type: application/json")
})
data = response.data
json_data = json.loads(data)
# Then
self.assertEqual(response.status_code, 200)
self.assertEqual(json_data['status'], "SUCCESS")
During my practice I've found that approach quite good.
class TestCreateUser(TestCase):
def test_works_in_correct_case(self):
# equal to your test_create_user
#patch("usermgmt.app.services.user_service.UserService.create_new_user")
def test_handles_value_error_in_create_new_user(self, mock_create_new_user):
mock_create_new_user.side_effect = ValueError
# Your preparation flow here
response = self.client.post('/api/usermgmt/users') # simplified call here is just an example
self.assertEqual(response.status_code, 400)
# check if response body is what you've expected, etc.
The Python3 fetch_token method in this library does not check the response status before consuming the response. If the API call it makes fails, then the response will be invalid and the script crashes. Is there something I can set so that an exception will be raised on a non-success response before the library can read the response?
import requests
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient
from oauthlib.oauth2 import OAuth2Error
AUTH_TOKEN_URL = "https://httpstat.us/500" # For testing
AUTH = HTTPBasicAuth("anID", "aSecret")
CLIENT = BackendApplicationClient(client_id="anID")
SCOPES = "retailer.orders.write"
MAX_API_RETRIES = 4
class MyApp:
def __init__(self):
"""Initialize ... and obtain initial auth token for request"""
self.client = OAuth2Session(client=CLIENT)
self.client.headers.update(
{
"Content-Type": "application/json"
}
)
self.__authenticate()
def __authenticate(self):
"""Obtain auth token."""
server_errors = 0
# This needs more work. fetch_token is not raising errors but failing
# instead.
while True:
try:
self.token = self.client.fetch_token(
token_url=AUTH_TOKEN_URL, auth=AUTH, scope=SCOPES
)
break
except (OAuth2Error, requests.exceptions.RequestException) as e:
server_errors = MyApp.__process_retry(
server_errors, e, None, MAX_API_RETRIES
)
#staticmethod
def __process_retry(errors, exception, resp, max_retries):
# Log and process retries
# ...
return errors + 1
MyApp() # Try it out
You can add a "compliance hook" that will be passed the Response object from requests before the library attempts to parse it, like so:
def raise_on_error(response):
response.raise_for_status()
return response
self.client.register_compliance_hook('access_token_response', raise_on_error)
Depending on exactly when you may get errors, you might want to do this with 'refresh_token_response' and/or 'protected_request' as well. See the docstring for the register_compliance_hook method for more info.