Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking default=timezone.now for unit tests

I'm trying to write unit tests for a django app that does a lot of datetime operations. I have installed mock to monkey patch django's timezone.now for my tests.

While I am able to successfully mock timezone.now when it is called normally (actually calling timezone.now() in my code, I am not able to mock it for models that are created with a DateTimeField with default=timezone.now.


I have a User model that contains the following:

from django.utils import timezone ... timestamp = models.DateTimeField(default=timezone.now) modified = models.DateTimeField(default=timezone.now) ... def save(self, *args, **kwargs):     if kwargs.pop('modified', True):         self.modified = timezone.now()     super(User, self).save(*args, **kwargs) 

My unit test looks like this:

from django.utils import timezone  def test_created(self):     dt = datetime(2010, 1, 1, tzinfo=timezone.utc)     with patch.object(timezone, 'now', return_value=dt):         user = User.objects.create(username='test')         self.assertEquals(user.modified, dt)         self.assertEquals(user.timestamp, dt) 

assertEquals(user.modified, dt) passes, but assertEquals(user.timestamp, dt) does not.

How can I mock timezone.now so that even default=timezone.now in my models will create the mock time?


Edit

I know that I could just change my unit test to pass a timestamp of my choice (probably generated by the mocked timezone.now)... Curious if there is a way that avoids that though.

like image 771
dgel Avatar asked Sep 20 '13 19:09

dgel


People also ask

Should I mock everything in unit tests?

In a unit test, mock objects can simulate the behavior of complex, real objects and are therefore useful when it is impractical or impossible to incorporate a real object into a unit test. Mocking makes sense in a unit testing context.

How do you unit test a mock?

Advanced: Mocking in Unit TestMocking is used in unit tests to replace the return value of a class method or function. This may seem counterintuitive since unit tests are supposed to test the class method or function, but we are replacing all those processing and setting a predefined output.

What is mocking in unit testing Python?

Mocking is simply the act of replacing the part of the application you are testing with a dummy version of that part called a mock. Instead of calling the actual implementation, you would call the mock, and then make assertions about what you expect to happen.


2 Answers

Here's a method you can use that doesn't require altering your non-test code. Just patch the default attributes of the fields you want to affect. For example--

field = User._meta.get_field('timestamp') mock_now = lambda: datetime(2010, 1, 1) with patch.object(field, 'default', new=mock_now):     # Your code here 

You can write helper functions to make this less verbose. For example, the following code--

@contextmanager def patch_field(cls, field_name, dt):     field = cls._meta.get_field(field_name)     mock_now = lambda: dt     with patch.object(field, 'default', new=mock_now):         yield 

would let you write--

with patch_field(User, 'timestamp', dt):     # Your code here 

Similarly, you can write helper context managers to patch multiple fields at once.

like image 149
cjerdonek Avatar answered Sep 19 '22 00:09

cjerdonek


I just ran into this issue myself. The problem is that models are loaded before mock has patched the timezone module, so at the time the expression default=timezone.now is evaluated, it sets the default kwarg to the real timezone.now function.

The solution is the following:

class MyModel(models.Model):     timestamp = models.DateTimeField(default=lambda: timezone.now()) 
like image 36
Jordan Carlson Avatar answered Sep 19 '22 00:09

Jordan Carlson