I'm trying to mock out the method requests.get() of the module requests and set the attribute side_effect of the obtained instance of the Mock class.
I would like to associate a different status_code for each side effect value but I didn't succeed so far.
def test_func1(mocker):
side_effect = ["Ok",'','','Failed']
# This line should be changed
fake_resp.status_code = 200
fake_resp = mocker.Mock()
fake_resp.json = mocker.Mock(side_effect=side_effect)
mocker.patch("app.main.requests.get", return_value=fake_resp)
# func1 executes multiple API calls using requests.get()
# and status_code is needed
a = func1(a, b)
assert a == "something"
I have not been able to find a way (in the doc and SO) to associate the status_code for each mock request.
I was thinking about something like this but it's obviously not working:
def test_func1(mocker):
side_effect = [(status_code=200, return="Ok"),
(status_code=204, return=""),
(status_code=204, return=""),
(status_code=500, return="Failed")]
....
EDIT: add the code of func1()
from datetime import datetime, timedelta
import requests
def func1(days, delta_1):
"""
days: number of days before first search (80, 25, 3)
delta_1: number of days for the date range search (40, 20, 15)
"""
now = datetime.now()
start_date = now + timedelta(days=days)
# Var to stop loop when price is found
loop_stop = 0
# Var to stop loop when search date is more than a year later
delta_time = 0
price = 0
departureDate = "n/a"
# For loop to check prices till one year.
while loop_stop == 0 and delta_time < (365 - days):
date_range = (
(start_date + timedelta(days=delta_time)).strftime("%Y%m%d")
+ "-"
+ (start_date + timedelta(days=delta_time + (delta_1 / 2))).strftime(
"%Y%m%d"
)
)
# Needs to be mocked
response = requests.get("fake_url_using_date_range_var")
if response.status_code == 204:
print("No data found on this data range")
delta_time += delta_1
elif response.status_code == 200:
price = response.json()["XXX"][0]
departureDate = response.json()["YYY"][0]
loop_stop = 1
else:
raise NameError(
response.status_code,
"Error occured while querying API",
response.json(),
)
return price, departureDate
Possible solution with module unittest (not pytest)
I have written this answer before #baguette added the code of its function func1(), so I have created a file called my_file_01.py which contains my production function func1():
import requests
def func1():
response1 = 'empty1'
response2 = 'empty2'
r = requests.get('http://www.someurl.com')
if r.status_code == 200:
response1 = r.json()
r = requests.get('http://www.some_other_url.com')
if r.status_code == 500:
response2 = r.json()
return [response1, response2]
As you can see func1() calls requests.get() two times and checks the status code of responses.
I have inserted the test code in a different file with the following content:
import unittest
from unittest import mock
from my_file_01 import func1
def request_resp1(url):
response_mock = mock.Mock()
response_mock.status_code = 200
response_mock.json.return_value = {'key1': 'value1'}
# the function return an instance of class Mock
return response_mock
def request_resp2(url):
response_mock = mock.Mock()
response_mock.status_code = 500
response_mock.json.return_value = "Failed"
# the function return an instance of class Mock
return response_mock
class TestFunc1(unittest.TestCase):
#mock.patch("my_file_01.requests")
def test_func1(self, mock_requests):
print("test func1()")
mock_requests.get.side_effect = [request_resp1(""), request_resp2("")]
[response1, response2] = func1()
print("response1 = " + str(response1))
print("response2 = " + str(response2))
if __name__ == "__main__":
unittest.main()
The test file defines the test class TestFunc1 which contains the test method test_func1().
Furthermore the file defines 2 functions called request_resp1() and request_resp2().
These functions are used to define different response values when it is called the method requests.get() by the code of func1(); to be more accurate:
the first call to requests.get() assigns to variable r the Mock object with status_code = 200 so it simulates a success;
the second call to requests.get() assigns to variable r the Mock object with status_code = 500 so it simulates a failure.
If you try to execute the test code you can see that func1() return different values for the 2 different status_codes of the response. The output of the execution is composed by the 3 print() instructions present in test_func1() and is the followed:
test func1()
response1 = {'key1': 'value1'}
response2 = Failed
Useful links
For detail about side_effect attribute see its documentation.
request is a Python module and this is a minimal documentation about it.
Based on the solution from #frankfalse, the two mocking functions can be replaced by a class.
class MockResponse:
def __init__(self, json_data, status_code=requests.codes.ok):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
With the previous class the file contained the test code of #frankfalse becomes:
import unittest
from unittest import mock
from my_file_01 import func1
import requests
class MockResponse:
def __init__(self, json_data, status_code=requests.codes.ok):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
class TestFunc1(unittest.TestCase):
#mock.patch("my_file_01.requests")
def test_func1(self, mock_requests):
print("test func1()")
mock_requests.get.side_effect = [MockResponse({'key1': 'value1'}), MockResponse('Failed', 500)]
[response1, response2] = func1()
print("response1 = " + str(response1))
print("response2 = " + str(response2))
The differences are:
the mocking functions request_resp1() and request_resp2() can be removed
it is necessary to add: import requests for the presence of the assignment status_code=requests.codes.ok in the __init__() method
the instruction for the definition of side_effect becomes:
mock_requests.get.side_effect = [MockResponse({'key1': 'value1'}), MockResponse('error', 500)]
Because ith has been created the class MockResponse, the class Mock of the module unittest.mock is not used.
Related
I have two functions:
job_status is getting a response from boto3 api.
jobs_detailsis a list comprehension that performs job_status on each element of the input list.
I want to change jobs_details into a decorator of jobs_status but below solutions throws inner() takes 1 positional argument but 2 were given error.
Appreciate any comment/alternative approach to my issue. Thanks!
import boto3
class GlueClient:
def __init__(self):
self.glue_client = boto3.client('glue')
#self.envs = envs
def jobs_list(self):
response = self.glue_client.list_jobs()
result = response["JobNames"]
while "NextToken" in response:
response = self.glue_client.list_jobs(NextToken=response["NextToken"])
result.extend(response["JobNames"])
return [e for e in result if "jobs_xyz" in e]
#WHAT IS CURRENTLY
def job_status(self, job_name):
paginator = self.glue_client.get_paginator('get_job_runs')
response = paginator.paginate(JobName=job_name)
return response
def jobs_details(self, jobs):
return [self.job_status(e) for e in jobs]
#WHAT IS EXPECTED
def pass_by_list_comprehension(func):
def inner(list_of_val):
return [func(value) for value in list_of_val ]
return inner
#pass_by_list_comprehension
def job_status(self, job_name):
paginator = self.glue_client.get_paginator('get_job_runs')
response = paginator.paginate(JobName=job_name)
return response
glue_client = GlueClient()
jobs = glue_client.jobs_list()
jobs_status = glue_client.job_status(jobs)
print(jobs)
You want something like:
import boto3
from typing import Callable
def handle_iterable_input(func):
def inner(self, list_of_val):
return [func(self, value) for value in list_of_val]
return inner
class GlueClient:
def __init__(self):
self.glue_client = boto3.client('glue')
#self.envs = envs
def jobs_list(self):
response = self.glue_client.list_jobs()
result = response["JobNames"]
while "NextToken" in response:
response = self.glue_client.list_jobs(NextToken=response["NextToken"])
result.extend(response["JobNames"])
return [e for e in result if "jobs_xyz" in e]
#handle_iterable_input
def job_status(self, job_name):
paginator = self.glue_client.get_paginator('get_job_runs')
response = paginator.paginate(JobName=job_name)
return response
glue_client = GlueClient()
jobs = glue_client.jobs_list()
jobs_status = glue_client.job_status(jobs)
print(jobs)
This is the most basic way to make your decorator handle methods properly, by explicitly handling the passing of self. Note, it assumes the function being decorated will only take a single argument.
If all you want to do is make job_status iterate through a list of job names instead of operating on just one, something like this should work:
def jobs_status(self, job_names):
paginator = self.glue_client.get_paginator('get_job_runs')
return [paginator.paginate(JobName=job_name) for job_name in job_names]
Using a decorator to change what parameters a method expects seems like a bad idea.
Also, naming your class GlueClient would imply that it is a glue client. The fact that it has an attribute named glue_client makes me suspect you could probably choose a clearer name for one or both of them. (However, I'm not familiar with the package you're using.)
I have the following code:
def verify_pseudo_streaming(self, publishedName, path, start):
cname = self.get_cname(publishedName)
params = {'start': start}
url = 'http://{}{}'.format(cname, path)
origin_size = int(requests.head(url).headers['Content-Length'])
start_headers = requests.head(url, params=params).headers
start_size = int(start_headers['Content-Length'])
msg = "Start size is not lower than origin size"
assert start_size < origin_size, msg
In my test I have mocked the requests.head in my unit test, how do I get the value of headers the first and second time when running requests.head without really running it ?
I finally ended up doing the one below which worked ...
class MockHeaders(object):
def __init__(self):
pass
def streaming_headers(self, *args, **kwargs):
start = kwargs.get('params', {})
self.headers['Content-Length'] = start.get('start', 10)
stuff = Mock()
stuff.headers = self.headers
return stuff
<snip> ... </snip>
#patch("FrontEnd.requests.head")
#patch("FrontEnd.FrontEnd.get_cname")
def test_verify_pseudo_streaming(self, mock_get_cname,mock_head):
mock_get_cname.return_value = 'hulahoop'
mock_header = MockHeaders()
mock_head.side_effect = mock_header.streaming_headers
mock_head.return_value = mock_header
try:
self.fe.verify_pseudo_streaming('publishedName', 'path', 5)
except AssertionError:
self.fail("Unexpected Assertion Error")
I am just going to keep this open to see if others got other more elegant ideas.
you can mock \ monkeypatch only this method
requests.sessions.Session.send
this is what requests use to send the request, so if you change that to do nothing
it will not send the request
def x(*args, **kwarg):
pass
requests.sessions.Session.send = x
I would mock out requests like this:
class FakeHeaders(object):
def __init__(self):
self.headers = {'Content-Length': 1}
def inc_headers():
self.headers['Content-Length'] += 1
def testVerifyPsuedoStreaming(self):
fake_header = FakeHeader()
with mock.patch.object(
request, 'head', side_effect=fake_header.inc_headers,
return_value=fake_header) as mock_head:
...
Trying to understand mocking/patching and I have a restful API project with three files (FYI, I'm using flask)
class1.py
domain.py
test_domain.py
class1.py file content:
class one:
def addition(self):
return 4+5
domain.py file content:
from class1 import one
class DomainClass(Resource):
def post(self):
test1 = one()
val = test1.addition()
return {'test' : val }
test_domain.py file content:
import my_app
from flask_api import status
from mock import patch
app = my_app.app.test_client()
def test_post():
with patch('domain.one') as mock:
instance = mock.return_value
instance.addition.return_value = 'yello'
url = '/domain'
response = app.post(url)
print response.data
assert status.HTTP_200_OK == response.status_code
assert mock.called
For my test_domain.py file, I've also tried this...
#patch('domain.one')
def test_post(mock_domain):
mock_domain.addition.return_value = 1
url = '/domain'
response = app.post(url)
print response.data
assert status.HTTP_200_OK == response.status_code
My assert for the status of 200 passes, however, the problem is that I'm not able to mock or patch the addition method to give me value of 1 in place of 9 (4+5). I also tried doing 'assert mock.called' and it failes as well. I know I should be mocking/patching where the 'one()' method is used, i.e. in domain.py not in class1.py. But I tried even mocking class1.one in place of domain.one and I still kept getting 9 and not 1. What am I doing wrong ?
******** Update
I've another dilemma on the same issue, I tried doing this in the test_domain file instead of patching....
from common.class1 import one
def test_post():
one.addition = MagicMock(return_value=40)
url = '/domain'
response = app.post(url)
print response.data
assert status.HTTP_200_OK == response.status_code
Question
In update above, I did not do a mock at the place where it is used (i.e.: domain.one.addition = MagicMock(...) and it still worked !!!! It seems it may be doing a global change. Why did this work ?
In the above example, 'one' is a class in the module class1.py. If I change this class 'one' to a function in class1.py, mocking does not work. It seems this function 'one' residing in module class1.py can not be mocked like this...one.return_value = 'xyz', why? Can it be mocked globally ?
There are some issues in your code. In the first example you forgot that patch() is applied in with context and the original code is recovered when the context end. Follow code should work:
def test_post():
with patch('domain.one') as mock:
instance = mock.return_value
instance.addition.return_value = 'yello'
url = '/domain'
response = app.post(url)
print response.data
assert status.HTTP_200_OK == response.status_code
assert mock.called
assert response.data['test'] == 'yello'
The second one have an other issue: if you want patch just addition method you should use:
#patch('domain.one.addition')
def test_post(mock_addition):
mock_addition.return_value = 1
...
assert mock_addition.called
assert response.data['test'] == 1
If you want mock all one class you should set the return value of addition method of mock instance returned by mock_domain call like in your first example:
#patch('domain.one')
def test_post(mock_domain):
mock_addition = mock_domain.return_value.addition
mock_addition.return_value = 1
...
assert mock_addition.called
assert response.data['test'] == 1
When using the following code only for demonstrative purposes:
from uuid import uuid4
class router(object):
def route(self):
res = response(jid=str(uuid4()))
worker = resource()
worker.dispatch(res)
print '[jid: %s, status: %s]' % (res.jid, res.status)
class response(object):
def __init__(self, jid):
self.jid = jid
self.status = 0
class resource(object):
def __init__(self):
self.status = 200
def dispatch(self, res):
res.status = self.status
rs = 'ok'
#return rs
yield rs
app = router()
app.route()
If using return rs instead of yield rs I can update the value of status within the dispatch(self, res) method of the resource class, out put is something like:
[jid: 575fb301-1aa9-40e7-a077-87887c8be284, status: 200]
But if using yield rs I can't update the value of status, I am getting always the original value, example:
[jid: 575fb301-1aa9-40e7-a077-87887c8be284, status: 0]
Therefore I would like to know, how to update the object variables of an object passed as a reference, when using yield.
You need to iterate the generator. Otherwise the generator is not executed.
>>> def gen():
... print(1)
... yield 'blah'
... print(2)
...
>>> g = gen() # No print (not executed)
>>> next(g) # print 1, yield `blah`. execution suspended.
1
'blah'
Replace following line:
worker.dispatch(res)
with:
for rs in worker.dispatch(res):
pass
or
next(worker.dispatch(res))
By using yield you are telling python that your dispatch() method is a generator.
So when you call worker.dispatch(res), nothing actually happens (try to call print worker.dispatch(res), you'll see just the object reference).
You have to iterate over it as mentioned by falsetru.
See also the Python yield keyword explained
I need to write a unit test using Mock module for a Python code with different functions:
_read_file(): opens a URL and then parse the xml using Element tree and return element tree instance.
validate_field: this filed takes arguments as field_value and order.
tree.findall function is used to get all fields_values in a list and then using the order exact value of that field is stored in a field. this file is then compared with the received argument and based on comparison either true of false is returned.
I have written unit test for above function. I have mocked urllib.urlopen and the return value of the called function, but it's giving error as:
IndexError: list index out of range
Please guide me on this.
Code below:
class test:
def __init__(self, host, service_path='/policy/test.xml'):
self.base_url = 'https://' +host + service_path
self.service_path = service_path
self.host = host
def _read_policy(self, url):
policy_xml = urllib.urlopen(url)
self.tree = ElementTree.parse(policy_xml)
return self.tree
def compare(self, arg1, arg2):
if arg1 == arg2:
return 'true'
else:
return 'false'
def validate_email(self, email, order):
index = int(order) - 1
email_list = self.tree.findall("testList/test/email")
xml_email = email_list[index].text
_resp = self.compare(xml_email, email)
return(_resp)
Test class:
class test_validate_email(unittest.TestCase):
def test_validate_email(self):
myMock = Mock( return_value = StringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?><email>*#test.com</email>"""))
urllib.urlopen = myMock
print 'urllib.urlopen', urllib.urlopen()
cp = test(DEFAULT_HOST)
tree = cp._read_policy("some data")
ElementTree.dump(tree)
myMock2 = Mock(return_value = 'true')
_resp = myMock2
print 'test_resp', _resp()
resp = cp.validate_email('*#ashishtest.com','1')
print 'resp', resp
self.assertEqual(resp, 'true')
if __name__ == '__main__':
unittest.main()