I am new to unittest.mock library and unable to solve the issue I am experiencing.
I have a class called ‘function.py’ in the below folder structure
src
_ init.py
function.py
tests
init.py
test_function.py
In test_function.py I have some code like this:
import unittest
from unittest import mock
from ..src.function import get_subscriptions
from ..src import function
class TestCheckOrder(unittest.TestCase):
#mock.patch.object(function, 'table')
def test_get_subscriptions_success(self, mocked_table):
mocked_table.query.return_value = []
user_id = "test_user"
status = True
get_subscriptions(user_id, status)
mocked_table.query.assert_called_with(
KeyConditionExpression=conditions.Key('user_id').eq(user_id),
FilterExpression=conditions.Attr('status').eq(int(status)))
In function.py:
import boto3
from boto3.dynamodb import conditions
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("Subscriptions")
def get_subscriptions(user_id, active=True):
results = table.query(
KeyConditionExpression=conditions.Key(
'user_id').eq(user_id),
FilterExpression=conditions.Attr('status').eq(int(active))
)
return results['Items']
If I run this I get the following exception:
**AssertionError: Expected call: query(FilterExpression=<boto3.dynamodb.conditions.Equals object at 0x1116011d0>, KeyConditionExpression=<boto3.dynamodb.conditions.Equals object at 0x111601160>)
Actual call: query(FilterExpression=<boto3.dynamodb.conditions.Equals object at 0x1116010f0>, KeyConditionExpression=<boto3.dynamodb.conditions.Equals object at 0x111601080>)**
Thanks in advance for helping me out.
The issue is that when you're calling assert_called_with in your test, you're creating new instances of conditions.Key and conditions.Attr. And as these instances are different from one we had in the actual call, there's a mismatch(check the hex ids shown in the traceback).
Instead of this you can fetch the kwargs from the function call itself and test their properties:
name, args, kwargs = mocked_table.query.mock_calls[0]
assert kwargs['KeyConditionExpression'].get_expression()['values'][1] == user_id
assert kwargs['FilterExpression'].get_expression()['values'][1] == int(status)
Related
I have a function like below.
# in retrieve_data.py
import os
def create_output_csv_file_path_and_name(output_folder='outputs') -> str:
"""
Creates an output folder in the project root if it doesn't already exist.
Then returns the path and name of the output CSV file, which will be used
to write the data.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
logging.info(f"New folder created for output file: " f"{output_folder}")
return os.path.join(output_folder, 'results.csv')
I also created a unit test file like below.
# in test_retrieve_data.py
class OutputCSVFilePathAndNameCreationTest(unittest.TestCase):
#patch('path.to.retrieve_data.os.path.exists')
#patch('path.to.retrieve_data.os.makedirs')
def test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet(
self,
os_path_exists_mock,
os_makedirs_mock
):
os_path_exists_mock.return_value = False
retrieve_cradle_profile_details.create_output_csv_file_path_and_name()
os_path_exists_mock.assert_called_once()
os_makedirs_mock.assert_called_once()
But when I run the above unit test, I get the following error.
def assert_called_once(self):
"""assert that the mock was called only once.
"""
if not self.call_count == 1:
msg = ("Expected '%s' to have been called once. Called %s times.%s"
% (self._mock_name or 'mock',
self.call_count,
self._calls_repr()))
raise AssertionError(msg)
AssertionError: Expected 'makedirs' to have been called once. Called 0 times.
I tried poking around with pdb.set_trace() in create_output_csv_file_path_and_name method and I'm sure it is receiving a mocked object for os.path.exists(), but the code never go pasts that os.path.exists(output_folder) check (output_folder was already created in the program folder but I do not use it for unit testing purpose and want to keep it alone). What could I possibly be doing wrong here to mock os.path.exists() and os.makedirs()? Thank you in advance for your answers!
You have the arguments to your test function reversed. When you have stacked decorators, like:
#patch("retrieve_data.os.path.exists")
#patch("retrieve_data.os.makedirs")
def test_create_output_csv_file_path_...():
They apply bottom to top, so you need to write:
#patch("retrieve_data.os.path.exists")
#patch("retrieve_data.os.makedirs")
def test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet(
self, os_makedirs_mock, os_path_exists_mock
):
With this change, if I have this in retrieve_data.py:
import os
import logging
def create_output_csv_file_path_and_name(output_folder='outputs') -> str:
"""
Creates an output folder in the project root if it doesn't already exist.
Then returns the path and name of the output CSV file, which will be used
to write the data.
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder)
logging.info(f"New folder created for output file: " f"{output_folder}")
return os.path.join(output_folder, 'results.csv')
And this is test_retrieve_data.py:
import unittest
from unittest.mock import patch
import retrieve_data
class OutputCSVFilePathAndNameCreationTest(unittest.TestCase):
#patch("retrieve_data.os.path.exists")
#patch("retrieve_data.os.makedirs")
def test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet(
self, os_makedirs_mock, os_path_exists_mock
):
os_path_exists_mock.return_value = False
retrieve_data.create_output_csv_file_path_and_name()
os_path_exists_mock.assert_called_once()
os_makedirs_mock.assert_called_once()
Then the tests run successfully:
$ python -m unittest -v
test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet (test_retrieve_data.OutputCSVFilePathAndNameCreationTest.test_create_output_csv_file_path_and_name_calls_exists_and_makedirs_once_when_output_folder_is_not_created_yet) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Update I wanted to leave a comment on the diagnostics I performed here, because I didn't initially spot the reversed arguments, either, but the problem became immediately apparent when I added a breakpoint() the beginning of the test and printed out the values of the mocks:
(Pdb) p os_path_exists_mock
<MagicMock name='makedirs' id='140113966613456'>
(Pdb) p os_makedirs_mock
<MagicMock name='exists' id='140113966621072'>
The fact that the names were swapped made the underlying problem easy to spot.
I'm relatively new to pytest and unit tests in general. Specifically I'm struggling to implement mocking, but I do understand the concept. I have a Class, let's call it MyClass. It has a constructor which takes a number of arguments used by other functions within MyClass.
I have a get_tables() function that I have to test and it relies on some arguments defined in the constructor. I need to mock the BigQuery connection and return a mocked list of tables. A small snippet of the script is below
from google.cloud import bigquery
from google.cloud import storage
import logging
class MyClass:
def __init__(self, project_id: str, pro_dataset_id: str, balancing_dataset_id: str, files: list,
key_file: str = None, run_local=True):
"""Creates a BigQuery client.
Args:
project_id: (string), name of project
pro_dataset_id: (string), name of production dataset
balancing_dataset_id: (string), name of balancing dataset
files: (list), list of tables
key_file: (string), path to the key file
"""
if key_file is None:
self.bq_client = bigquery.Client(project=project_id)
self.storage_client = storage.Client(project=project_id)
self.bucket = self.storage_client.get_bucket("{0}-my-bucket".format(project_id))
else:
self.bq_client = bigquery.Client.from_service_account_json(key_file)
self.project_id = project_id
self.run_local = run_local
self.pro_dataset_id = pro_dataset_id
self.balancing_dataset_id = balancing_dataset_id
def get_tables(self):
"""Gets full list of all tables in a BigQuery dataset.
Args:
Returns:
List of tables from a specified dataset
"""
full_table_list = []
dataset_ref = '{0}.{1}'.format(self.project_id, self.pro_dataset_id)
tables = self.bq_client.list_tables(dataset_ref) # Make an API request.
logging.info(f"Tables contained in '{dataset_ref}':")
for table in tables:
full_table_list.append(table.table_id)
logging.info(f"tables: {full_table_list}")
return full_table_list
This was my attempt at mocking the connection and response based on an amalgamation of articles I've read and stackoverflow answers on other questions including this one How can I mock requests and the response?
import pytest
from unittest import mock
from MyPackage import MyClass
class TestMyClass:
def mocked_list_tables():
tables = ['mock_table1', 'mock_table2', 'mock_table3', 'mock_table4']
return tables
#mock.patch('MyPackage.MyClass', side_effect=mocked_list_tables)
def test_get_tables(self):
m = MyClass()
assert m.get_tables() == ['mock_table1', 'mock_table2', 'mock_table3', 'mock_table4']
This is the error I get with the above unit test
TypeError: test_get_tables() takes 1 positional argument but 2 were given
How do I get this test to work correctly? Incase you're wondering, the arguments in the constructor are declared using argparse.ArgumentParser().add_argument()
I have a function in myfile.py that accepts no arguments. Inside the function I am creating a pandas DataFrame based on the data returned from an API call shown as: df = pd.DataFrame(api_call_from_helper_function().all_the_data()). I am trying to mock the API call so it doesn't try to actually connect to the API. Below is my attempt:
myfile.py
def make_dataframe_from_data():
df = pd.DataFrame(api_call_from_helper_function().all_the_data())
return df
test_myfile.py
import pandas as pd
from unittest.mock import MagicMock
import myfile
from myfile import make_dataframe_from_data
def test_make_dataframe_from_data(monkeypatch):
mock_df = MagicMock()
mock_instance = MagicMock(return_value=mock_df)
monkeypatch.setattr("pd.DataFrame", mock_instance)
result = make_dataframe_from_data()
mock_instance.assert_called_once()
assert isinstance(result, pd.DataFrame())
In this code, the api_call_from_helper_function() uses another function I made to make the api call and the appended .all_the_data() returns all the data that it got. Right now my tests can mock the api call in api_call_from_helper_function() but I can't get it to mock when I pass it as a parameter to pd.DataFrame. What am I doing wrong?
Can you please help me out to figure what I did wrong? I have the following unit test for a python lambdas
class Tests(unittest.TestCase):
def setUp(self):
//some setup
#mock.patch('functions.tested_class.requests.get')
#mock.patch('functions.helper_class.get_auth_token')
def test_tested_class(self, mock_auth, mock_get):
mock_get.side_effect = [self.mock_response]
mock_auth.return_value = "some id token"
response = get_xml(self.event, None)
self.assertEqual(response['statusCode'], 200)
The problem is that when I run this code, I get the following error for get_auth_token:
Invalid URL '': No schema supplied. Perhaps you meant http://?
I debugged it, and it doesn't look like I patched it correctly. The Authorization helper file is in the same folder "functions" as the tested class.
EDIT:
In the tested_class I was importing get_auth_token like this:
from functions import helper_class
from functions.helper_class import get_auth_token
...
def get_xml(event, context):
...
response_token = get_auth_token()
After changing to this, it started to work fine
import functions.helper_class
...
def get_xml(event, context):
...
response_token = functions.helper_class.get_auth_token()
I still don't fully understand why though
In your first scenario
in tested_class.py, get_auth_token is imported
from functions.helper_class import get_auth_token
The patch should be exactly the get_auth_token at tested_class
#mock.patch('functions.tested_class.get_auth_token')
Second scenario
With the following usage
response_token = functions.helper_class.get_auth_token()
The only way to patch is this
#mock.patch('functions.helper_class.get_auth_token')
alternative
With import like this in tested_class
from functions import helper_class
helper_class.get_auth_token()
patch could be like this:
#mock.patch('functions.tested_class.helper_class.get_auth_token')
patch() works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work, you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.
Python documentation has a very good example. where to patch
I am really stuck with this. I have created a new class in Odoo that I want call from a controller. This object needs to get data about a customer (res.partner) when I pass it an id field that I have created.
The problem I have is that I can't seem to call my parse method in my class. However I do it I get a nonetype object has not attribute parse.
What am I doing wrong? Am I a noob? And also am I making it harder than it needs to be?
Here is my class in a file called callback.py
__author__ = 'karl'
import requests
import json
from openerp import models, api
import logging
_logger = logging.getLogger(__name__)
class JiraParse(models.Model):
_name = "res.parter"
_inherit = "res.partner"
def readname(self,jira_id):
query = """
SELECT name
FROM res.partner
WHERE jira_id = {0}
""".format(jira_id)
self.env.cr.execute(query)
result = [(row[0], row[0]) for row in self.env.cr.fetchall()]
_logger.info(str(result))
return result
def parse(self,data):
#load json data
R = json.loads(data)
Customer = R['issue']['fields']['customfield_10128']
CustomerId = R['issue']['fields']['customfield_10128']['id']
issue_url = R['issue']['self']
res = self.readname(CustomerId)
_logger.info(str(res))
#create dictionary/json callback object
json_response = {'fields':
{'customfield_10128':{'value': 'ISYS Group'}
}}
#json_response = Customer,CustomerId,issue_url
#Make call back request to Jira to update customer data
H = {'Content-Type':'application/json'}
req = requests.post('http://10.10.15.39:5000', data=json.dumps(json_response), headers=H)
return True
I am trying to call it from my controller like this:
t = callback.JiraParse()
t.parse(requestdata)
Where requestdata is a json object received by the controller.
All I get is this
AttributeError: 'NoneType' object has no attribute 'parse'
Thanks
You can do something like this for calling your class method using class object.
t = JiraParse()
t.parse(requestdata)
Main reason behind for not calling your method directly because your method is not the static method so that we must have to make that parse method as static for directly access by your class name . so it is totally part of object oriented concept.
If it is in separate file then you must have to import the JiraParse class in your .py file.
from callback import JiraParse
t = JiraParse()
t.parse(requestdata)
I hope my answer may helpful for you :)