Python different mocking behaviour depending the code style - python

I found a strange behavior of mocking. Could you please explain me where is the pitfall?
Searching on the net for examples for mocking i found the above piece of code, which works.
But if i rewrite it as unittest.TestCase subclass, the mocking does not works.
import datetime
from unittest.mock import Mock
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)
def is_weekday():
today = datetime.datetime.today()
print( "Today is %d/%d/%d"%(today.day, today.month, today.year))
return (0 <= today.weekday() < 5)
datetime = Mock()
datetime.datetime.today.return_value = tuesday
assert is_weekday()
datetime.datetime.today.return_value = saturday
assert not is_weekday()
The above code will produce as expected the following
Today is 1/1/2019
Today is 5/1/2019
I rewrote the above code as subclass of unittest.TestCase
import unittest
import datetime
from unittest.mock import Mock
tuesday = datetime.datetime(year=2019, month=1, day=1)
saturday = datetime.datetime(year=2019, month=1, day=5)
def is_weekday():
today = datetime.datetime.today()
return (0 <= today.weekday() < 5)
class MyTestCase(unittest.TestCase):
def test_something(self):
datetime = Mock()
datetime.datetime.today.return_value = tuesday
assert is_weekday()
datetime.datetime.today.return_value = saturday
assert not is_weekday()
if __name__ == '__main__':
unittest.main()
But now the output is different
Today is 16/7/2021
Today is 16/7/2021

Python is lexically scoped; is_weekday uses a global name datetime, not the local name datetime you assigned the Mock to.
class MyTestCase(unittest.TestCase):
def test_something(self):
global datetime
old_datetime = datetime
datetime = Mock()
datetime.datetime.today.return_value = tuesday
assert is_weekday()
datetime.datetime.today.return_value = saturday
assert not is_weekday()
datetime = old_datetime
Better, though, would be to use unittest.mock.patch
from unittest.mock import patch
class MyTestCase(unittest.TestCase):
#patch('datetime.datetime')
def test_something(self, mock_dt):
mock_dt.today.return_value = tuesday
assert is_weekday()
mock_dt.today.return_value = saturday
assert not is_weekday()

Related

How to use patch.object() correctly

I'm trying to get my head around Mock and patch(), but I'm stuck on a somewhat simple example. Say I have the following function in main.py, which tests if it's a weekday or not.
from datetime import datetime
def is_weekday():
today = datetime.today()
return (0 <= today.weekday() < 5)
I want to run my test with two possible outcomes: either False if I mock Saturday or Sunday or True if it's a weekday. Now, I'm clearly not mocking anyting when calling main.is_weekday so my test currently fails as it's the weekend. How can I fix that?
from unittest.mock import Mock, patch
import pytest
import main
def test_weekday():
datetime = Mock()
tuesday = datetime(year=2019, month=1, day=1)
datetime.today.return_value = tuesday
with patch.object(datetime, "main.is_weekday", tuesday) as mock_method:
expected = True
actual = main.is_weekday() # how do I mock datetime.today()?
assert actual == expected
The essential problem is that you're not patching datetime in your main module. The first argument to patch.object is the thing you want to patch, and since you're passing in your datetime Mock object, that doesn't do you any good.
I would restructure your test like this:
from unittest.mock import Mock, patch
from datetime import datetime
from mockito import when, ANY
import main
def test_weekday():
testdate = datetime(year=2019, month=1, day=1)
with patch("main.datetime", wraps=datetime) as mock_datetime:
mock_datetime.today.return_value = testdate
assert main.is_weekday()
def test_weekend():
testdate = datetime(year=2019, month=1, day=5)
with patch("main.datetime", wraps=datetime) as mock_datetime:
mock_datetime.today.return_value = testdate
assert not main.is_weekday()
Here, we're replacing main.datetime with a mock object, and then configuring it such that calling datetime.today() in the main module will return a specific date.
Then we test that the code works as expected for both weekdays and
weekends.

timedelta issue in python3.9 vs.python3.6

I have the following class in a file called 'GPS_Date.py':
import datetime
from math import floor
class GPS_Date(datetime.datetime):
ref_date = datetime.datetime(1980, 1, 6)
def __init__(self, year, month, day, hour=0, minute=0, second=0):
datetime.datetime.__init__(year, month, day, hour, minute, second)
def gps_week(self):
difftime = self-self.ref_date
return floor(difftime.days / 7)
def day_of_week(self):
difftime = self-self.ref_date
return difftime.days % 7
def day_of_year(self):
return self.timetuple().tm_yday
#staticmethod
def to_GPS_date(date):
return GPS_Date(date.year, date.month, date.day, date.hour, date.minute, date.second)
#staticmethod
def now():
return GPS_Date.to_GPS_date(datetime.datetime.utcnow())
When I run the following code in python3.6 I get the correct solution:
import datetime
from GPS_Date import GPS_Date
time_string = '2019-01-01 23:59:30.0'
date_format = '%Y-%m-%d %H:%M:%S.%f'
time_1 = datetime.datetime.strptime(time_string, date_format)
time_2 = GPS_Date.to_GPS_date(time_1)
add_time = time_2 + datetime.timedelta(minutes=30)
But when I run it with python3.9 I get the following error:
add_time = time_2 + datetime.timedelta(minutes=30)
TypeError: __init__() takes from 4 to 7 positional arguments but 9 were given
I assume something has been changed between python3.6 and python3.9. I've looked at documentation but haven't found anything. Can anyone enlighten me?
datetime.datetime does have more arguments that can be passed than GPS_Date accounts for (i.e. tzinfo and fold). Why this doesn't blow up in Python3.6, I am not sure. But you don't need to override __init__ at all, since you aren't doing anything:
class GPS_Date(datetime.datetime):
ref_date = datetime.datetime(1980, 1, 6)
def gps_week(self):
difftime = self - self.ref_date
return floor(difftime.days / 7)
def day_of_week(self):
difftime = self - self.ref_date
return difftime.days % 7
def day_of_year(self):
return self.timetuple().tm_yday
#staticmethod
def to_GPS_date(date):
return GPS_Date(date.year, date.month, date.day, date.hour, date.minute, date.second)
#staticmethod
def now():
return GPS_Date.to_GPS_date(datetime.datetime.utcnow())
is perfectly fine. (Also note: If you were to do something, you need to override __new__ instead of __init__)

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

Python 3.xx : Classes in classes

I need help for my python scripts. How to access my clock's function through Date's classes ?
from datetime import date
from datetime import datetime
class Date(object):
def date_today(self):
now = date.today()
print (now)
class Time(Date):
pass
def clock(self):
hr = datetime.now()
hr_now = hr.hour
print (hr_now)
cr_date = Date()
print (cr_date.date_today())
print (cr_date.date_today.clock())
i got an error --> AttributeError: 'function' object has no attribute 'clock'. What is the reason for this error?
you can also add minute, second and other related functions in your time class. I hope it will help.
from datetime import date
from datetime import datetime
class Time():
def clock(self):
hr = datetime.now()
hr_now = hr.hour
return hr_now
class Date():
def __init__(self):
self.time = Time()
def date_today(self):
now = date.today()
return now
def clock(self):
return self.time.clock()
cr_date = Date()
print(cr_date.date_today())
print(cr_date.clock())

Printing correct time using timezones, Python

Extends
Ok, we are not having a good day today.
When you attach the correct tzinfo object to a datetime instance, and then you strftime() it, it STILL comes out in UTC, seemingly ignoring the beautiful tzinfo object I attached to it.
# python 2.5.4
now = datetime.now()
print now.strftime( "%a %b %d %X" ) # %X is "locale's appropriate time rep"
pst = now.replace( tzinfo=Pacific )
print pst.strftime( "%a %b %d %X" )
We get:
Mon Jan 18 17:30:16
Mon Jan 18 17:30:16
I found if I add %z, I can add the difference its supposed to have computed:
Mon Jan 18 17:32:38
Mon Jan 18 17:32:38 -0800
It just tacks on the -8 there, as if to say, "you do it yourself, foo."
But I want strftime() to simply give me a string WITH PRECOMPUTED LOCAL TIME.
How can I get strftime() to do the hour subtraction math for me when I strftime() it?
The full code I'm using is below.
from datetime import tzinfo, timedelta, datetime
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
# A UTC class.
class UTC(tzinfo):
"""UTC"""
def utcoffset(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def dst(self, dt):
return ZERO
utc = UTC()
# A class building tzinfo objects for fixed-offset time zones.
# Note that FixedOffset(0, "UTC") is a different way to build a
# UTC tzinfo object.
class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC."""
def __init__(self, offset, name):
self.__offset = timedelta(minutes = offset)
self.__name = name
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return ZERO
# A class capturing the platform's idea of local time.
import time as _time
STDOFFSET = timedelta(seconds = -_time.timezone)
if _time.daylight:
DSTOFFSET = timedelta(seconds = -_time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
class LocalTimezone(tzinfo):
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
return _time.tzname[self._isdst(dt)]
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
stamp = _time.mktime(tt)
tt = _time.localtime(stamp)
return tt.tm_isdst > 0
Local = LocalTimezone()
# A complete implementation of current DST rules for major US time zones.
def first_sunday_on_or_after(dt):
days_to_go = 6 - dt.weekday()
if days_to_go:
dt += timedelta(days_to_go)
return dt
# In the US, DST starts at 2am (standard time) on the first Sunday in April.
DSTSTART = datetime(1, 4, 1, 2)
# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
# which is the first Sunday on or after Oct 25.
DSTEND = datetime(1, 10, 25, 1)
class USTimeZone(tzinfo):
def __init__(self, hours, reprname, stdname, dstname):
self.stdoffset = timedelta(hours=hours)
self.reprname = reprname
self.stdname = stdname
self.dstname = dstname
def __repr__(self):
return self.reprname
def tzname(self, dt):
if self.dst(dt):
return self.dstname
else:
return self.stdname
def utcoffset(self, dt):
return self.stdoffset + self.dst(dt)
def dst(self, dt):
if dt is None or dt.tzinfo is None:
# An exception may be sensible here, in one or both cases.
# It depends on how you want to treat them. The default
# fromutc() implementation (called by the default astimezone()
# implementation) passes a datetime with dt.tzinfo is self.
return ZERO
assert dt.tzinfo is self
# Find first Sunday in April & the last in October.
start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
# Can't compare naive to aware objects, so strip the timezone from
# dt first.
if start <= dt.replace(tzinfo=None) < end:
return HOUR
else:
return ZERO
Eastern = USTimeZone(-5, "Eastern", "EST", "EDT")
#Central = USTimeZone(-6, "Central", "CST", "CDT")
#Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
Pacific = USTimeZone(-8, "Pacific", "PST", "PDT")
now = datetime.now()
print now.strftime( "%a %b %d %X %z" )
pst = now.replace( tzinfo=Pacific )
print pst.strftime( "%a %b %d %X %z" )
.replace does no computation: it simply replaces one or more field in the new returned object, while copying all others from the object it's called on.
If I understand your situation correctly, you start with a datetime object which you know (through other means) is UTC, but doesn't know that itself (is has a tzinfo attribute of None, meaning "I'm totally clueless regarding what timezone I'm in).
So, first, you make a timezone-aware from your input timezone-naive object, in order to inform it that it's in timezone UTC (all other fields just get copied over):
aware = naive.replace(tzinfo=utc)
Then, you can request computations regarding timezones, and printing in consequence:
print aware.astimezone(Pacific).strftime('%a %b %d %X %z')
With dt.replace(tzinfo=tz) you're not really converting the time value, you're just saying 'hey no, wait, this time was actually in PDT, not in UTC'. You'll probably want to use datetime.astimezone(tz) instead.
I think Wim had the right idea, just backwards. If you want to know what your time would be in UTC, use:
print pst.astimezone(UTC).strftime( "%a %b %d %X" )
You'll have to dig up a definition for a UTC timezone class. I understand why Python didn't want to supply a default implementation of every possible tzinfo, but UTC should have been included in the base package.

Categories

Resources