Simulating the passing of time in unittesting
Question:
I’ve built a paywalled CMS + invoicing system for a client and I need to get more stringent with my testing.
I keep all my data in a Django ORM and have a bunch of Celery tasks that run at different intervals that makes sure that new invoices and invoice reminders get sent and cuts of access when users don’t pay their invoices.
For example I’d like to be a able to run a test that:
-
Creates a new user and generates an invoice for X days of access to the site
-
Simulates the passing of X + 1 days, and runs all the tasks I’ve got set up in Celery.
-
Checks that a new invoice for an other X days has been issued to the user.
The KISS approach I’ve come up with so far is to do all the testing on a separate machine and actually manipulate the date/time at the OS-level. So the testing script would:
-
Set the system date to day 1
-
Create a new user and generate the first invoice for X days of access
-
Advance then system date 1 day. Run all my celery tasks. Repeat until X + 1 days have “passed”
-
Check that a new invoice has been issued
It’s a bit clunky but I think it might work. Any other ideas on how to get it done?
Answers:
You can use mock to change the return value of the function you use to get the time (datetime.datetime.now
for example).
There are various ways to do so (see the mock documentation), but here is one :
import unittest
import datetime
from mock import patch
class SomeTestCase(unittest.TestCase):
def setUp(self):
self.time = datetime.datetime(2012, 5, 18)
class fakedatetime(datetime.datetime):
@classmethod
def now(cls):
return self.time
patcher = patch('datetime.datetime', fakedatetime)
self.addCleanup(patcher.stop)
patcher.start()
def test_something(self):
self.assertEqual(datetime.datetime.now(), datetime.datetime(2012, 5, 18))
self.time = datetime.datetime(2012, 5, 20)
self.assertEqual(datetime.datetime.now(), datetime.datetime(2012, 5, 20))
Because we can’t replace directly datetime.datetime.now
, we create a fake datetime class that does everything the same way, except returning a constant value when now is called.
Without the use of a special mock library, I propose to prepare the code for being in mock-up-mode (probably by a global variable). In mock-up-mode instead of calling the normal time-function (like time.time() or whatever) you could call a mock-up time-function which returns whatever you need in your special case.
I would vote down for changing the system time. That does not seem like a unit test but rather like a functional test as it cannot be done in parallel to anything else on that machine.
You can also take a look at freezegun
module. Github – https://github.com/spulec/freezegun
From their docs
from freezegun import freeze_time
import datetime
@freeze_time("2012-01-14")
def test():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
Here’s @madjar’s technique as a reusable context manager, and it mocks up the time
module at the same time in case your stuff uses that rater than datetime.now()
:
import datetime
import time
import mock
from contextlib import contextmanager
@contextmanager
def mock_passage_of_time(
hours: int = 0,
minutes: int = 0,
seconds: int = 0
):
now = datetime.datetime.now()
mock_now = now + datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)
class MockDateTime(datetime.datetime):
@classmethod
def now(cls):
return mock_now
with mock.patch('datetime.datetime', MockDateTime),
mock.patch('time.time', mock.MagicMock(return_value=mock_now.timestamp())):
yield
Usage:
class SomeTestCase(unittest.TestCase):
def test_something(self):
orig_now = datetime.datetime.now()
five_mins_in_the_future = orig_now + timedelta(minutes=5)
with mock_passage_of_time(minutes=5):
delta = datetime.datetime.now() - five_mins_in_the_future
self.assertEqual(delta.seconds, 0)
I’ve built a paywalled CMS + invoicing system for a client and I need to get more stringent with my testing.
I keep all my data in a Django ORM and have a bunch of Celery tasks that run at different intervals that makes sure that new invoices and invoice reminders get sent and cuts of access when users don’t pay their invoices.
For example I’d like to be a able to run a test that:
-
Creates a new user and generates an invoice for X days of access to the site
-
Simulates the passing of X + 1 days, and runs all the tasks I’ve got set up in Celery.
-
Checks that a new invoice for an other X days has been issued to the user.
The KISS approach I’ve come up with so far is to do all the testing on a separate machine and actually manipulate the date/time at the OS-level. So the testing script would:
-
Set the system date to day 1
-
Create a new user and generate the first invoice for X days of access
-
Advance then system date 1 day. Run all my celery tasks. Repeat until X + 1 days have “passed”
-
Check that a new invoice has been issued
It’s a bit clunky but I think it might work. Any other ideas on how to get it done?
You can use mock to change the return value of the function you use to get the time (datetime.datetime.now
for example).
There are various ways to do so (see the mock documentation), but here is one :
import unittest
import datetime
from mock import patch
class SomeTestCase(unittest.TestCase):
def setUp(self):
self.time = datetime.datetime(2012, 5, 18)
class fakedatetime(datetime.datetime):
@classmethod
def now(cls):
return self.time
patcher = patch('datetime.datetime', fakedatetime)
self.addCleanup(patcher.stop)
patcher.start()
def test_something(self):
self.assertEqual(datetime.datetime.now(), datetime.datetime(2012, 5, 18))
self.time = datetime.datetime(2012, 5, 20)
self.assertEqual(datetime.datetime.now(), datetime.datetime(2012, 5, 20))
Because we can’t replace directly datetime.datetime.now
, we create a fake datetime class that does everything the same way, except returning a constant value when now is called.
Without the use of a special mock library, I propose to prepare the code for being in mock-up-mode (probably by a global variable). In mock-up-mode instead of calling the normal time-function (like time.time() or whatever) you could call a mock-up time-function which returns whatever you need in your special case.
I would vote down for changing the system time. That does not seem like a unit test but rather like a functional test as it cannot be done in parallel to anything else on that machine.
You can also take a look at freezegun
module. Github – https://github.com/spulec/freezegun
From their docs
from freezegun import freeze_time
import datetime
@freeze_time("2012-01-14")
def test():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
Here’s @madjar’s technique as a reusable context manager, and it mocks up the time
module at the same time in case your stuff uses that rater than datetime.now()
:
import datetime
import time
import mock
from contextlib import contextmanager
@contextmanager
def mock_passage_of_time(
hours: int = 0,
minutes: int = 0,
seconds: int = 0
):
now = datetime.datetime.now()
mock_now = now + datetime.timedelta(hours=hours, minutes=minutes, seconds=seconds)
class MockDateTime(datetime.datetime):
@classmethod
def now(cls):
return mock_now
with mock.patch('datetime.datetime', MockDateTime),
mock.patch('time.time', mock.MagicMock(return_value=mock_now.timestamp())):
yield
Usage:
class SomeTestCase(unittest.TestCase):
def test_something(self):
orig_now = datetime.datetime.now()
five_mins_in_the_future = orig_now + timedelta(minutes=5)
with mock_passage_of_time(minutes=5):
delta = datetime.datetime.now() - five_mins_in_the_future
self.assertEqual(delta.seconds, 0)