Monkeypatch imported module from other file with pytest - python

As the title says. I want to mock an imported function from a module. In this case the module is datetime and I want to mock datetime.datetime.now. I included what I've done without success.
# main.py:
import datetime
# cannot modify
def call_me(func):
return func()
class A:
variable = call_me(datetime.datetime.now)
# conftest.py:
import pytest
import datetime
FAKE_TIME = datetime.datetime.fromisoformat("2020-03-19T03:30:00")
#pytest.fixture(autouse=True)
def patch_datetime_now(monkeypatch):
class mydatetime(datetime.datetime):
#classmethod
def now(cls):
return FAKE_TIME
monkeypatch.setattr('datetime.datetime', mydatetime)
# test_main.py:
import datetime
from main import A
def test_main():
assert A.variable == datetime.datetime.now()
❯ pytest
------------------------------------------------------------------------------------
def test_main():
> assert A.variable == datetime.datetime.now()
E AssertionError: assert datetime.datetime(2020, 3, 19, 21, 32, 39, 861956) == datetime.datetime(2020, 3, 19, 3, 30)
I searched for a workaround for this and only found this question How to monkeypatch python's datetime.datetime.now with py.test?. The code included is just a minimal example of what I need. Basically I have an app who uses sqlalchemy.orm for saving data and a specific model have a default value for the date equal to datetime.datetime.now and I need to modify the date of the default value to be able to test it. The models are defined in other file.
Thank you for your help.

Related

Python unit tests - mocking imported class methods

I would like to mock some imported class methods and module functions for my unit tests. I tried several ways to define the mocked values but I don't understand why they are not taken into account.
I wrote some tests following the advices in Python Mocking a function from an imported module.
Here is a piece of code representing the application to test:
from services import myModule1
from services.spec1 import importedClass
class myClass(object):
def __init__(self, param1, param2):
self.param1 = param1
self.param2 = param2
self.param3 = 0
self.param4 = 0
self.myMethod()
def myMethod(self):
newVar = importedClass()
self.param3 = newVar.meth1(self.param2)
calcParam = myModule1.methodMod1(self.param1)
self.param4 = calcParam["keyParam3"]
I would like to unit test myMethod and I need to mock importedClass and myModule1.methodMod1().
Here is a new piece of code that I tried for the tests (previous attempts below):
import unittest
from unittest.mock import patch
from my_module import myClass
class test_myClass(unittest.TestCase):
#patch('my_module.importedClass')
#patch('my_module.myModule1')
def test_myMethod(self, mock_mod1, mock_class):
mock_mod1.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
mock_class.meth1.return_value = 2
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)
if __name__ == '__main__':
unittest.main()
The mocking has no error but the mocked values are not taken into account.
Previous attempts
What I tried for each of them, using 2 different approaches:
import unittest
from unittest.mock import Mock, patch
from my_module import myClass
import services
class test_myClass(unittest.TestCase):
def setUp(self):
services.spec1 = Mock()
services.spec1.importedClass.meth1.return_value = 2
#patch('services.myModule1')
def test_myMethod(self, my_mock):
my_mock.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)
if __name__ == '__main__':
unittest.main()
The result is that the calculated attributes are not updated and still 0 - so test fails.
I also tried with services = Mock() and defined return values for each part, to regroup each mock in setUp method or in a #patch, but nothing worked.
I also tried with my_module.spec1 = Mock(), to make the function global, or even self.spec1 = Mock() to make it very local to the test's context (if I understood correctly the differences, this is something I'm not really sure neither) but nothing worked.
To mock just the method:
class test_myClass(unittest.TestCase):
# #patch('my_module.importedClass') # Change this
#patch('my_module.importedClass.meth1') # to this
#patch('my_module.myModule1')
# def test_myMethod(self, mock_mod1, mock_class): # Change this
def test_myMethod(self, mock_mod1, mock_class_meth1): # to this
mock_mod1.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
# mock_class.meth1.return_value = 2 # Change this
mock_class_meth1.return_value = 2 # to this
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)
To mock the class and its method:
https://docs.python.org/3/library/unittest.mock-examples.html#mocking-classes
class test_myClass(unittest.TestCase):
#patch('my_module.importedClass')
#patch('my_module.myModule1')
def test_myMethod2(self, mock_mod1, mock_class):
mock_mod1.methodMod1.return_value = {"keyParam3": 5, "keyParam4": 7}
# mock_class.meth1.return_value = 2 # Change this
mock_class_instance = mock_class.return_value # to these
mock_class_instance.meth1.return_value = 2 # two lines
test_parameters = (0, 0)
test_res = myClass(*test_parameters)
self.assertEqual(test_res.param3, 2)
self.assertEqual(test_res.param4, 5)

pytest - fake time changes during test

I have the following code to set a faketime during tests.
I'ld like to change the time during a test. That is, the test should start at 9:00 for isntance and then continue as if it is 10:00 .
from __future__ import annotations
import datetime
import logging
import pytest
LOGGER = logging.getLogger(__name__)
#pytest.fixture(params=[datetime.datetime(2020, 12, 25, 17, 5, 55)])
def patch_datetime_now(request, monkeypatch):
class mydatetime(datetime.datetime):
#classmethod
def now(cls):
return request.param
class mydate(datetime.date):
#classmethod
def today(cls):
return request.param.date()
monkeypatch.setattr(datetime, "datetime", mydatetime)
monkeypatch.setattr(datetime, "date", mydate)
#pytest.mark.usefixtures("patch_datetime_now")
#pytest.mark.parametrize(
"patch_datetime_now", [(datetime.datetime(2020, 12, 9, 11, 22, 00))], indirect=True
)
def test_update_data():
fakeTime = datetime.datetime.now()
# Do some stuff
# Change the fake time
# Do some other stuff
How can I change the fake time during the test. The 'datetime' is used inside the code tested, so it's not about changing the "fakeTime" variable contents, but about changing the time returned by the datetime mockup.
Maybe I need to change the mocking method completely, I am just sharing my current code.
Following this answer on another question provided by #MrBeanBremen I updated my code like this:
from __future__ import annotations
import datetime
import logging
import pytest
LOGGER = logging.getLogger(__name__)
#pytest.fixture(params=[datetime.datetime(2020, 12, 25, 17, 5, 55)])
def patch_datetime_now(request, monkeypatch):
def _delta(timedelta=None, **kwargs):
""" Moves time fwd/bwd by the delta"""
from datetime import timedelta as td
if not timedelta:
timedelta = td(**kwargs)
request.param += timedelta
class mydatetime(datetime.datetime):
#classmethod
def now(cls):
return request.param
#classmethod
def delta(cls,*args,**kwargs):
_delta(*args,**kwargs)
class mydate(datetime.date):
#classmethod
def today(cls):
return request.param.date()
#classmethod
def delta(cls,*args,**kwargs):
_delta(*args,**kwargs)
monkeypatch.setattr(datetime, "datetime", mydatetime)
monkeypatch.setattr(datetime, "date", mydate)
#pytest.mark.usefixtures("patch_datetime_now")
#pytest.mark.parametrize(
"patch_datetime_now", [(datetime.datetime(2020, 12, 9, 11, 22, 00))], indirect=True
)
def test_update_data():
fakeTime = datetime.datetime.now()
assert fakeTime == datetime.datetime(2020, 12, 9, 11, 22, 00)
datetime.datetime.delta(hours=1,seconds=10)
fakeTime = datetime.datetime.now()
assert fakeTime == datetime.datetime(2020, 12, 9, 12, 22, 10)

How to unit test a method that contains a database call in Python

I want to unit test a method that includes a database (SQL Server) call.
I don't want the test to connect to the actual database.
I use unittest for testing, I have done some research and it seems that Mocking could do the trick but not sure about the syntax.
The select statement on the code below returns some integers. I guess that mocking will target the "cursor.execute" and "cursor.fetchall()" parts of the code.
from databaselibrary.Db import Db
class RandomClass():
def __init__(self, database):
self.database = database # Main DB for inserting data
def check_file_status(self, trimmed_file_data, file_date):
cursor = self.database.cursor()
cursor.execute(f"""SELECT DISTINCT query_id
FROM wordcloud_count
WHERE date = '{file_date}'""")
queries_in_DB = set(row.query_id for row in cursor.fetchall())
queries_in_file = set(trimmed_file_data.keys())
if queries_in_DB == queries_in_file:
return False
return True
def run(self):
print("Hello")
if __name__ == "__main__":
connection_string = 'sql://user:password#server/database'
database = Db(connection_string, autocommit=True)
random = RandomClass(database)
random.run()
The test class could look like that:
import unittest
from unittest.mock import Mock, patch
from project.RandomClass import RandomClass
from datetime import datetime
class testRandomClass(unittest.TestCase):
def setUp(self):
self.test_class = RandomClass("don't want to put actual database here")
#patch("project.RandomClass.check_file_status",return_value={123, 1234})
def test_check_file_status(self):
keys = {'1234':'2','123':'1','111':'5'}
result = self.test_class.check_file_status(keys, datetime(1900, 1, 1, 23, 59, 59))
self.assertTrue(result)
You should mock the db connection object, and the cursor. Then, set the return value of the cursor to return the expected value. I've tested the below code, and used class Row to mock the rows returned from fetchall call:
import unittest
from unittest.mock import MagicMock
from datetime import datetime
from project.RandomClass import RandomClass
class Row(object):
def __init__(self, x):
self.query_id = x
class testRandomClass(unittest.TestCase):
def setUp(self):
dbc = MagicMock(name="dbconn")
cursor = MagicMock(name="cursor")
cursor.fetchall.return_value = [Row(1), Row(2)]
dbc.cursor.return_value = cursor
self.test_class = RandomClass(dbc)
def test_check_file_status(self):
keys = {'1234': '2', '123': '1', '111': '5'}
result = self.test_class.check_file_status(keys, datetime(1900, 1, 1, 23, 59, 59))
self.assertTrue(result)
Since in your RandomClass you iterate rows and get their query_id, you need to use a class (or a named tuple) as the row objects returned by the mock.
You should create the row objects you expected, and set them as the return value of fetchall.

assert_called_once_with() with datetime.now() in the call parameter?

I have the following test code to test Decorator.
#mock.patch('a.b.c.KafkaProducer')
def test_1(self, mocked):
decorator = Decorator(....)
#decorator()
def test():
return 42
test()
start_time = ???
v = {'type': 'batch', 'start_time': start_time.isoformat()}
mocked.return_value.send.assert_called_once_with(value=v)
However, the test always fail because Decorator calls mocked with dictionary parameter with property of start_time assigned to datetime.now(). Is it a way to compare everything except start_time? Or any other way to test the call?
Two practical approaches:
freeze time using https://pypi.org/project/freezegun/
import datetime
from freezegun import freeze_time
from unittest.mock import patch
import my_library
NOT_NOW = datetime.datetime.now()
#freeze_time("2020-01-01")
#patch("my_library.helper")
def test_foo(_helper):
my_library.under_test(NOT_NOW)
# kinda auto-magic
_helper.assert_called_once_with(datetime.datetime.now())
# or more explicitly
_helper.assert_called_once_with(datetime.datetime(2020, 1, 1))
or, evaluate arguments manually
#patch("my_library.helper", return_value=42)
def test_bar(_helper):
my_library.under_test(NOT_NOW)
assert _helper.call_count == 1
assert _helper.call_args[0][0]
assert _helper.call_args[0][0] != NOT_NOW

How to make global imports from a function?

I fear that this is a messy way to approach the problem but...
let's say that I want to make some imports in Python based on some conditions.
For this reason I want to write a function:
def conditional_import_modules(test):
if test == 'foo':
import onemodule, anothermodule
elif test == 'bar':
import thirdmodule, and_another_module
else:
import all_the_other_modules
Now how can I have the imported modules globally available?
For example:
conditional_import_modules(test='bar')
thirdmodule.myfunction()
Imported modules are just variables - names bound to some values. So all you need is to import them and make them global with global keyword.
Example:
>>> math
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'math' is not defined
>>> def f():
... global math
... import math
...
>>> f()
>>> math
<module 'math' from '/usr/local/lib/python2.6/lib-dynload/math.so'>
You can make the imports global within a function like this:
def my_imports(module_name):
globals()[module_name] = __import__(module_name)
I've just had the similar problem, here is my solution:
class GlobalImport:
def __enter__(self):
return self
def __call__(self):
import inspect
self.collector = inspect.getargvalues(inspect.getouterframes(inspect.currentframe())[1].frame).locals
def __exit__(self, *args):
globals().update(self.collector)
then, anywhere in the code:
with GlobalImport() as gi:
import os, signal, atexit, threading, _thread
# whatever you want it won't remain local
# if only
gi()
# is called before the end of this block
# there you go: use os, signal, ... from whatever place of the module
You can use the built-in function __import__ to conditionally import a module with global scope.
To import a top level module (think: import foo):
def cond_import():
global foo
foo = __import__('foo', globals(), locals())
Import from a hierarchy (think: import foo.bar):
def cond_import():
global foo
foo = __import__('foo.bar', globals(), locals())
Import from a hierarchy and alias (think: import foo.bar as bar):
def cond_import():
global bar
foo = __import__('foo.bar', globals(), locals())
bar = foo.bar
I like #badzil approach.
def global_imports(modulename,shortname = None, asfunction = False):
if shortname is None:
shortname = modulename
if asfunction is False:
globals()[shortname] = __import__(modulename)
else:
globals()[shortname] = eval(modulename + "." + shortname)
So something that is traditionally in a class module:
import numpy as np
import rpy2
import rpy2.robjects as robjects
import rpy2.robjects.packages as rpackages
from rpy2.robjects.packages import importr
Can be transformed into a global scope:
global_imports("numpy","np")
global_imports("rpy2")
global_imports("rpy2.robjects","robjects")
global_imports("rpy2.robjects.packages","rpackages")
global_imports("rpy2.robjects.packages","importr",True)
May have some bugs, which I will verify and update. The last example could also have an alias which would be another "shortname" or a hack like "importr|aliasimportr"
I like #rafał grabie approach. As it even support importing all.
i.e.
from os import *
(Despite it being bad practice XD )
Not allowed to comment, but here is a python 2.7 version.
Also removed the need to call the function at the end.
class GlobalImport:
def __enter__(self):
return self
def __exit__(self, *args):
import inspect
collector = inspect.getargvalues(inspect.getouterframes(inspect.currentframe())[1][0]).locals
globals().update(collector)
def test():
with GlobalImport() as gi:
## will fire a warning as its bad practice for python.
from os import *
test()
print path.exists(__file__)
I like the answer from #maxschlepzig.
There is a bug in the approach that if you directly import a function it will not work.
For example,
global_imports("tqdm", "tqdm, True)
does not work, because the module is not imported. And this
global_imports("tqdm")
global_imports("tqdm", "tqdm, True)
works.
I change #maxschlepzig's answer a bit. Using fromlist so you can load function or module with "From" statement in a uniform way.
def global_imports(object_name: str,
short_name: str = None,
context_module_name: str = None):
"""import from local function as global import
Use this statement to import inside a function,
but effective as import at the top of the module.
Args:
object_name: the object name want to import,
could be module or function
short_name: the short name for the import
context_module_name: the context module name in the import
example usage:
import os -> global_imports("os")
import numpy as np -> global_imports("numpy", "np")
from collections import Counter ->
global_imports("Counter", None, "collections")
from google.cloud import storage ->
global_imports("storage", None, "google.cloud")
"""
if not short_name:
short_name = object_name
if not context_module_name:
globals()[short_name] = __import__(object_name)
else:
context_module = __import__(context_module_name,
fromlist=[object_name])
globals()[short_name] = getattr(context_module, object_name)
You could have this function return the names of the modules you want to import, and then use
mod == __import__(module_name)
Step-1: config.py, config_v2.py, rnd.py in same directory/folder
Step-2: config.py
HIGH_ATTENDANCE_COUNT_MIN = 0
Step-3: config_v2.py
HIGH_ATTENDANCE_COUNT_MIN = 5
Step-4: rnd.py
def versioning_test(v):
global config
if v == 'v1':
config = __import__('config', globals(), locals())
if v == 'v2':
config = __import__('config_v2', globals(), locals())
def version_test_in_another_function():
print('version_test_in_another_function: HIGH_ATTENDANCE_COUNT_MIN: ', config.HIGH_ATTENDANCE_COUNT_MIN)
versioning_test("v2")
version_test_in_another_function()
Step-5: $ python3 rnd.py
<<output>>: version_test_in_another_function: HIGH_ATTENDANCE_COUNT_MIN: 5
It is now recommended (for Python 3), to use the importlib
https://docs.python.org/3/reference/import.html#importlib
eg: globals()["np"] = importlib.import_module("numpy")
and you can now execute "np.array([1,2,3])" afterwards.
There are also other ways of importing that you might prefer. Consider seeing the aforementioned documentation.

Categories

Resources