I have the following module that I am trying to write unit tests for.
import myModuleWithCtxMgr
def myFunc(arg1):
with myModuleWithCtxMgr.ctxMgr() as ctxMgr:
result = ctxMgr.someFunc()
if result:
return True, result
return False, None
The unit tests I'm working on looks like this.
import mock
import unittest
import myModule as myModule
class MyUnitTests(unittest.TestCase):
#mock.patch("myModuleWithCtxMgr.ctxMgr")
def testMyFunc(self, mockFunc):
mockReturn = mock.MagicMock()
mockReturn.someFunc = mock.Mock(return_value="val")
mockFunc.return_value = mockReturn
result = myModule.myFunc("arg")
self.assertEqual(result, (True, "val"))
The test is failing because result[0] = magicMock() and not the return value (I thought) I configured.
I've tried a few different variations of the test but I can't seem to be able to mock the return value of ctxMgr.someFunc(). Does anyone know how I might accomplish this?
Thanks!
The error says:
First differing element 1:
<MagicMock name='ctxMgr().__enter__().someFunc()' id='139943278730000'>
'val'
- (True, <MagicMock name='ctxMgr().__enter__().someFunc()' id='139943278730000'>)
+ (True, 'val')
The error contains the mock name which exactly shows you what needs to be mocked. Note that __enter__ corresponds to the Context Manager protocol.
This works for me:
class MyUnitTests(unittest.TestCase):
#mock.patch("myModuleWithCtxMgr.ctxMgr")
def testMyFunc(self, mockCtxMgr):
mockCtxMgr().__enter__().someFunc.return_value = "val"
result = myModule.myFunc("arg")
self.assertEqual(result, (True, "val"))
Note how each of these is a separate MagicMock instance which you can configure:
mockCtxMgr
mockCtxMgr()
mockCtxMgr().__enter__
mockCtxMgr().__enter__()
mockCtxMgr().__enter__().someFunc
MagicMocks are created lazily but have identity, so you can configure them this way and it Just Works.
Related
I am not sure about the title of this question, as it is not easy to describe the issue with a single sentence. If anyone can suggest a better title, I'll edit it.
Consider this code that uses smbus2 to communicate with an I2C device:
# device.py
import smbus2
def set_config(bus):
write = smbus2.i2c_msg.write(0x76, [0x00, 0x01])
read = smbus2.i2c_msg.read(0x76, 3)
bus.i2c_rdwr(write, read)
I wish to unit-test this without accessing I2C hardware, by mocking the smbus2 module as best I can (I've tried mocking out the entire smbus2 module, so that it doesn't even need to be installed, but had no success, so I'm resigned to importing smbus2 in the test environment even if it's not actually used - no big deal so far, I'll deal with that later):
# test_device.py
# Depends on pytest-mock
import device
def test_set_config(mocker):
mocker.patch('device.smbus2')
smbus = mocker.MagicMock()
device.set_config(smbus)
# assert things here...
breakpoint()
At the breakpoint, I'm inspecting the bus mock in pdb:
(Pdb) p smbus
<MagicMock id='140160756798784'>
(Pdb) p smbus.method_calls
[call.i2c_rdwr(<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>, <MagicMock name='smbus2.i2c_msg.read()' id='140160757050688'>)]
(Pdb) p smbus.method_calls[0].args
(<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>, <MagicMock name='smbus2.i2c_msg.read()' id='140160757050688'>)
(Pdb) p smbus.method_calls[0].args[0]
<MagicMock name='smbus2.i2c_msg.write()' id='140160757018400'>
Unfortunately, at this point, the arguments that were passed to write() and read() have been lost. They do not seem to have been recorded in the smbus mock and I've been unable to locate them in the data structure.
Interestingly, if I break in the set_config() function, just after write and read assignment, and inspect the mocked module, I can see:
(Pdb) p smbus2.method_calls
[call.i2c_msg.write(118, [160, 0]), call.i2c_msg.read(118, 3)]
(Pdb) p smbus2.method_calls[0].args
(118, [160, 0])
So the arguments have been stored as a method_call in the smbus2 mock, but not copied over to the smbus mock that is passed into the function.
Why is this information not retained? Is there a better way to test this function?
I think this can be summarised as this:
In [1]: from unittest.mock import MagicMock
In [2]: foo = MagicMock()
In [3]: bar = MagicMock()
In [4]: w = foo.write(1, 2)
In [5]: r = foo.read(1, 2)
In [6]: bar.func(w, r)
Out[6]: <MagicMock name='mock.func()' id='140383162348976'>
In [7]: bar.method_calls
Out[7]: [call.func(<MagicMock name='mock.write()' id='140383164249232'>, <MagicMock name='mock.read()' id='140383164248848'>)]
Note that the bar.method_calls list contains calls to the functions .write and .read (good), but the parameters that were passed to those functions are missing (bad). This seems to undermine the usefulness of such mocks, since they don't interact as I would expect. Is there a better way to handle this?
The reason you can't access the calls to write and read is that they themselves are the return_value of another mock. What you are trying to do is access the mock "parent" (Using the terminology here: https://docs.python.org/3/library/unittest.mock.html).
It actually is possible to access the parent, but I'm not sure it's a good idea, since it used an undocumented and private attribute of the MagicMock object, _mock_new_parent.
def test_set_config(mocker):
"""Using the undocumented _mock_new_parent attribute"""
mocker.patch('device.smbus2')
smbus = mocker.MagicMock()
device.set_config(smbus)
# Retrieving the `write` and `read` values passed to `i2c_rdwr`.
mocked_write, mocked_read = smbus.i2c_rdwr.call_args[0]
# Making some assertions about how the corresponding functions were called.
mocked_write._mock_new_parent.assert_called_once_with(0x76, [0x00, 0x01])
mocked_read._mock_new_parent.assert_called_once_with(0x76, 3)
You can check that the assertions work by using some bogus values instead, and you'll see the pytest assertion errors.
A simpler, and more standard approach IMO is to look at the calls from the module mock directly:
def test_set_config_2(mocker):
""" Using the module mock directly"""
mocked_module = mocker.patch('device.smbus2')
smbus = mocker.MagicMock()
device.set_config(smbus)
mocked_write = mocked_module.i2c_msg.write
mocked_read = mocked_module.i2c_msg.read
mocked_write.assert_called_once_with(0x76, [0x00, 0x01])
mocked_read.assert_called_once_with(0x76, 3)
I just realized that you use dependency injection and that you should take advantage of this.
This would be the clean approach.
Mocks can behave unexpected/nasty (which does not mean that they are evil - only sometime.... counterintuitive)
I would recommend following test structure:
# test_device.py
import device
def test_set_config():
dummy_bus = DummyBus()
device.set_config(dummy_bus)
# assert things here...
assert dummy_bus.read_data == 'foo'
assert dummy_bus.write_data == 'bar'
breakpoint()
class DummyBus:
def __init__(self):
self.read_data = None
self.write_data = None
def i2c_rdwr(write_input, read_input):
self.read_data = read_input
self.write_data = write_input
For anyone coming across this, I posed a variation of this problem in another question, and the result was quite satisfactory:
https://stackoverflow.com/a/73739343/
In a nutshell, create a TraceableMock class, derived from MagicMock, that returns a new mock that keeps track of its parent, as well as the parameters of the function call that led to this mock being created. Together, there is enough information to verify that the correct function was called, and the correct parameters were supplied.
I am trying to test a function called get_date_from_s3(bucket, table) using pytest. In this function, there a boto3.client("s3").list_objects_v2() call that I would like to mock during testing, but I can't seem to figure out how this would work.
Here is my directory setup:
my_project/
glue/
continuous.py
tests/
glue/
test_continuous.py
conftest.py
conftest.py
The code continuous.py will be executed in an AWS glue job but I am testing it locally.
my_project/glue/continuous.py
import boto3
def get_date_from_s3(bucket, table):
s3_client = boto3.client("s3")
result = s3_client.list_objects_v2(Bucket=bucket, Prefix="Foo/{}/".format(table))
# [the actual thing I want to test]
latest_date = datetime_date(1, 1, 1)
output = None
for content in result.get("Contents"):
date = key.split("/")
output = [some logic to get the latest date from the file name in s3]
return output
def main(argv):
date = get_date_from_s3(argv[1], argv[2])
if __name__ == "__main__":
main(sys.argv[1:])
my_project/tests/glue/test_continuous.py
This is what I want: I want to test get_date_from_s3() by mocking the s3_client.list_objects_v2() and explicitly setting the response value to example_response. I tried doing something like below but it doesn't work:
from glue import continuous
import mock
def test_get_date_from_s3(mocker):
example_response = {
"ResponseMetadata": "somethingsomething",
"IsTruncated": False,
"Contents": [
{
"Key": "/year=2021/month=01/day=03/some_file.parquet",
"LastModified": "datetime.datetime(2021, 2, 5, 17, 5, 11, tzinfo=tzlocal())",
...
},
{
"Key": "/year=2021/month=01/day=02/some_file.parquet",
"LastModified": ...,
},
...
]
}
mocker.patch(
'continuous.boto3.client.list_objects_v2',
return_value=example_response
)
expected = "20210102"
actual = get_date_from_s3(bucket, table)
assert actual == expected
Note
I noticed that a lot of examples of mocking have the functions to test as part of a class. Because continuous.py is a glue job, I didn't find the utility of creating a class, I just have functions and a main() that calls it, is it a bad practice? It seems like mock decorators before functions are used only for functions that are part of a class.
I also read about moto, but couldn't seem to figure out how to apply it here.
The idea with mocking and patching that one would want to mock/patch something specific. So, to have correct patching, one has to specify exactly the thing to be mocked/patch. In the given example, the thing to be patched is located in: glue > continuous > boto3 > client instance > list_objects_v2.
As you pointed one you would like calls to list_objects_v2() to give back prepared data. So, this means that you have to first mock "glue.continuous.boto3.client" then using the latter mock "list_objects_v2".
In practice you need to do something along the lines of:
from glue import continuous_deduplicate
from unittest.mock import Mock, patch
#patch("glue.continuous.boto3.client")
def test_get_date_from_s3(mocked_client):
mocked_response = Mock()
mocked_response.return_value = { ... }
mocked_client.list_objects_v2 = mocked_response
# Run other setup and function under test:
In the end, I figured out that my patching target value was wrong thanks to #Gros Lalo. It should have been 'glue.continuous.boto3.client.list_objects_v'. That still didn't work however, it threw me the error AttributeError: <function client at 0x7fad6f1b2af0> does not have the attribute 'list_objects_v'.
So I did a little refactoring to wrap the whole boto3.client in a function that is easier to mock. Here is my new my_project/glue/continuous.py file:
import boto3
def get_s3_objects(bucket, table):
s3_client = boto3.client("s3")
return s3_client.list_objects_v2(Bucket=bucket, Prefix="Foo/{}/".format(table))
def get_date_from_s3(bucket, table):
result = get_s3_objects(bucket, table)
# [the actual thing I want to test]
latest_date = datetime_date(1, 1, 1)
output = None
for content in result.get("Contents"):
date = key.split("/")
output = [some logic to get the latest date from the file name in s3]
return output
def main(argv):
date = get_date_from_s3(argv[1], argv[2])
if __name__ == "__main__":
main(sys.argv[1:])
My new test_get_latest_date_from_s3() is therefore:
def test_get_latest_date_from_s3(mocker):
example_response = {
"ResponseMetadata": "somethingsomething",
"IsTruncated": False,
"Contents": [
{
"Key": "/year=2021/month=01/day=03/some_file.parquet",
"LastModified": "datetime.datetime(2021, 2, 5, 17, 5, 11, tzinfo=tzlocal())",
...
},
{
"Key": "/year=2021/month=01/day=02/some_file.parquet",
"LastModified": ...,
},
...
]
}
mocker.patch('glue.continuous_deduplicate.get_s3_objects', return_value=example_response)
expected_date = "20190823"
actual_date = continuous_deduplicate.get_latest_date_from_s3("some_bucket", "some_table")
assert expected_date == actual_date
The refactoring worked out for me, but if there is a way to mock the list_objects_v2() directly without having to wrap it in another function, I am still interested!
In order to achieve this result using moto, you would have to create the data normally using the boto3-sdk. In other words: create a test case that succeeds agains AWS itself, and then slap the moto-decorator on it.
For your usecase, I imagine it looks something like:
from moto import mock_s3
#mock_s3
def test_glue:
# create test data
s3 = boto3.client("s3")
for d in range(5):
s3.put_object(Bucket="", Key=f"year=2021/month=01/day={d}/some_file.parquet", Body="asdf")
# test
result = get_date_from_s3(bucket, table)
# assert result is as expected
...
I have an instance method on a Django form class that returns a Python object from a payment service if successful.
The object has an id property, which I then persist on a Django model instance.
I'm having some difficulty getting the mocked object to return its .id property correctly.
# tests.py
class DonationFunctionalTest(TestCase):
def test_foo(self):
with mock.patch('donations.views.CreditCardForm') as MockCCForm:
MockCCForm.charge_customer.return_value = Mock(id='abc123')
# The test makes a post request to the view here.
# The view being tested calls:
# charge = credit_card_form.charge_customer()
# donation.charge_id = charge.id
# donation.save()
However:
print donation.charge_id
# returns
u"<MagicMock name='CreditCardForm().charge_customer().id'
I expected to see "abc123" for the donation.charge_id, but instead I see a unicode representation of the MagicMock. What am I doing wrong?
Got it working by doing the patching a bit differently:
#mock.patch('donations.views.CreditCardForm.create_card')
#mock.patch('donations.views.CreditCardForm.charge_customer')
def test_foo(self, mock_charge_customer, mock_create_card):
mock_create_card.return_value = True
mock_charge_customer.return_value = MagicMock(id='abc123')
# remainder of code
Now the id matches what I expect. I'd still like to know what I did wrong on the previous code though.
I am running tests with nose and would like to use a variable from one of the tests item in another. For this I create the variable when setting the class up. It seems to me that the variable is copied for each item, so the one in the class stays in fact untouched. If instead of a simple variable I use a list, I see the behavior that I was expecting.
I wrote a small exemple, we can observe that var1 and varg always show the same value when entering a test:
import time
import sys
import logging
logger = logging.getLogger('008')
class Test008:
varg = None
#classmethod
def setup_class(cls):
logger.info('* setup class')
cls.var1 = None
cls.list1 = []
def setup(self):
logger.info('\r\n* setup')
logger.info('\t var1: {}, varg: {}, list: {}'.format(
self.var1, self.varg, self.list1))
def teardown(self):
logger.info('* teardown')
logger.info('\t var1: {}, varg: {}, list: {}'.format(
self.var1, self.varg, self.list1))
def test_000(self):
self.var1 = 0
self.varg = 0
self.list1.append(0)
pass
def test_001(self):
# Here I would like to access the variables but they still show 'None'
self.var1 = 1
self.varg = 1
self.list1.append(1)
pass
#classmethod
def teardown_class(self):
logger.info('* teardown class')
Result:
nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$']
* setup class
008_TestVars.Test008.test_000 ...
* setup
var1: None, varg: None, list: []
* teardown
var1: 0, varg: 0, list: [0]
ok
008_TestVars.Test008.test_001 ...
* setup
var1: None, varg: None, list: [0]
* teardown
var1: 1, varg: 1, list: [0, 1]
ok
* teardown class
----------------------------------------------------------------------
Is there a way to have the values of var1 and varg be carried on from one test to the other?
The docs clearly say
a test case is constructed to run each method with a fresh instance of
the test class
If you need the actual time to set up an parameter to the function you are testing, why not write a test which sets up the state, call your function once and assert it passes then call it again and assert if fails?
def test_that_two_calls_to_my_method_with_same_params_fails(self):
var1 = 1
varg = 1
assert myMethod(var1, varg)
assert myMethod(var1, varg) == False
I think that is clearer because one test has all the state together, and can run the tests in any order.
You could argue it does, because you were trying to use the setup method. The docs also say
A test module is a python module that matches the testMatch regular
expression. Test modules offer module-level setup and teardown; define
the method setup, setup_module, setUp or setUpModule for setup,
teardown, teardown_module, or tearDownModule for teardown.
So, outside your class have a
def setup_module()
#bother now I need to use global variables
pass
I got a function in a certain module that I want to redefine(mock) at runtime for testing purposes. As far as I understand, function definition is nothing more than an assignment in python(the module definition itself is a kind of function being executed). As I said, I wanna do this in the setup of a test case, so the function to be redefined lives in another module. What is the syntax for doing this?
For example, 'module1' is my module and 'func1' is my function, in my testcase I have tried this (no success):
import module1
module1.func1 = lambda x: return True
import module1
import unittest
class MyTest(unittest.TestCase):
def setUp(self):
# Replace othermod.function with our own mock
self.old_func1 = module1.func1
module1.func1 = self.my_new_func1
def tearDown(self):
module1.func1 = self.old_func1
def my_new_func1(self, x):
"""A mock othermod.function just for our tests."""
return True
def test_func1(self):
module1.func1("arg1")
Lots of mocking libraries provide tools for doing this sort of mocking, you should investigate them as you will likely get a good deal of help from them.
import foo
def bar(x):
pass
foo.bar = bar
Just assign a new function or lambda to the old name:
>>> def f(x):
... return x+1
...
>>> f(3)
4
>>> def new_f(x):
... return x-1
...
>>> f = new_f
>>> f(3)
2
It works also when a function is from another module:
### In other.py:
# def f(x):
# return x+1
###
import other
other.f = lambda x: x-1
print other.f(1) # prints 0, not 2
Use redef: http://github.com/joeheyming/redef
import module1
from redef import redef
rd_f1 = redef(module1, 'func1', lambda x: True)
When rd_f1 goes out of scope or is deleted, func1 will go back to being back to normal
If you want to reload into the interpreter file foo.py that you are editing, you can make a simple-to-type function and use execfile(), but I just learned that it doesn't work without the global list of all functions (sadly), unless someone has a better idea:
Somewhere in file foo.py:
def refoo ():
global fooFun1, fooFun2
execfile("foo.py")
In the python interpreter:
refoo() # You now have your latest edits from foo.py