41

I have the below:

from datetime import datetime

def get_report_month_key():
    month_for_report = datetime.utcnow()
    return month_for_report.strftime("%Y%m") 

How do I mock datetime.utcnow() so that I can write unit test on this function?

Tried reading this one but I am unable to get it working for me on utcnow()

Community
  • 1
  • 1
Steven Yong
  • 5,163
  • 6
  • 35
  • 56

6 Answers6

59

in your test file:

from yourfile import get_report_month_key
import mock
import unittest
from datetime import datetime

class TestCase(unittest.TestCase):

    @mock.patch('yourfile.datetime')
    def test_dt(self, mock_dt):
        mock_dt.utcnow = mock.Mock(return_value=datetime(1901, 12, 21))
        r = get_report_month_key()
        self.assertEqual('190112', r)
dasjotre
  • 641
  • 5
  • 3
  • Best and simplest idea I've read so far if we simply want to mock it in one file! Thanks! – David Gourde Aug 25 '17 at 15:46
  • Of the four SO answers I tried, only this one worked. Thanks! – RedCraig Jan 17 '18 at 17:14
  • 2
    This would work as long as you're not using something else from `datetime`; otherwise see https://stackoverflow.com/a/51213128 (which I think should be the accepted answer). (Of course, a better approach altogether is to not use `datetime.utcnow()` directly, but wrap it in an injected service. As a compromise, the method described in https://stackoverflow.com/a/45799436 could be used.) – Tom Dec 06 '21 at 12:34
  • I had to read this several times to copy it successfully. All the other answers saying, "it cannot be done, mocking a builtin in one place mocks it everywhere", are wrong. This answer creates a meaningful `datetime` all while `datetime` is mocked elsewhere. Key to this is `import datetime` rather than `from datetime import datetime` in the code under test. – John Jan 10 '22 at 22:48
36

The accepted answer by dasjotre works if you don't create any datetime instances in the module you are testing. If you try to create a datetime it will create a Mock object instead of one with the expected methods on a standard datetime object. This is because it replaces the whole class definition with a mock. Instead of doing this, you can use a similar approach to create the mocked definition by using datetime as the base.

mymodule.py

from datetime import datetime

def after_y2k():
    y2k = datetime(2000, 1, 1)
    return y2k < datetime.utcnow()

test_mymodule.py

import unittest
import datetime
from mock import patch, Mock
import mymodule
from mymodule import after_y2k


class ModuleTests(unittest.TestCase):
    @patch.object(mymodule, 'datetime', Mock(wraps=datetime.datetime))
    def test_after_y2k_passes(self):
        # Mock the return and run your test (Note you are doing it on your module)
        mymodule.datetime.utcnow.return_value = datetime.datetime(2002, 01, 01)
        self.assertEqual(True, after_y2k())

        mymodule.datetime.utcnow.return_value = datetime.datetime(1999, 01, 01)
        self.assertEqual(False, after_y2k())

    @patch('mymodule.datetime')
    def test_after_y2k_fails(self, mock_dt):
        # Run your tests
        mock_dt.utcnow = Mock(return_value=datetime.datetime(2002, 01, 01))
        self.assertEqual(True, after_y2k())

        # FAILS!!! because the object returned by utcnow is a MagicMock w/o 
        # datetime methods like "__lt__"
        mock_dt.utcnow = Mock(return_value=datetime.datetime(1999, 01, 01))
        self.assertEqual(False, after_y2k())
Ryan Widmaier
  • 7,948
  • 2
  • 30
  • 32
15

What also works when patching built-in Python modules turns out to be complicated (as it is with datetime, see e.g. https://solidgeargroup.com/mocking-the-time or https://nedbatchelder.com/blog/201209/mocking_datetimetoday.html or https://gist.github.com/rbarrois/5430921) is wrapping the function in a custom one which then can be easily patched.

So, instead of calling datetime.datetime.utcnow(), you use a function like

import datetime


def get_utc_now():
    return datetime.datetime.utcnow()

Then, patching this one is as simple as

import datetime

# use whatever datetime you need here    
fixed_now = datetime.datetime(2017, 8, 21, 13, 42, 20)
with patch('your_module_name.get_utc_now', return_value=fixed_now):
    # call the code using get_utc_now() here
    pass

Using the patch decorator instead of the context manager would work similarly.

Dirk
  • 9,381
  • 17
  • 70
  • 98
  • This method worked best for me with using pytest and python 3.7 – Ralph Willgoss Jul 30 '20 at 09:03
  • 2
    Very handy. Note the warning from the [datetime documentation](https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow) however, so better to use `return datetime.datetime.now(datetime.timezone.utc)` *Because naive `datetime` objects are treated by many `datetime` methods as local times, it is preferred to use aware datetimes to represent times in UTC. As such, the recommended way to create an object representing the current time in UTC is by calling `datetime.now(timezone.utc)`.* – Andrew Richards Feb 11 '23 at 17:21
9

You can try using freezetime module.

from yourfile import get_report_month_key
from freezegun import freeze_time
import unittest

class TestCase(unittest.TestCase):

    @freeze_time('2017-05-01')
    def get_report_month_key_test():
       get_report_month_key().should.equal('201705')
leannez
  • 225
  • 3
  • 6
  • I am not sure that this could be an accepted answer, this answer introduce a new package dependency `feezegun` and only for test porpoises. It's not cool imho, but the good part it's that implements a Python decorator `@freeze_time` and it's more easier to code. – Franco Gil Nov 24 '22 at 14:28
0

The excepted answer works fine for most cases, I encountered one case where it doesn't work, when datatime is not called from a function.

Example:

time_0 = datetime.utcnow()

class Service():
  time_1 = datetime.utcnow()
  
  def __init__(self):
      time_2 = datetime.utcnow()

Here time_0, time_1 will not be mocked. To fix this use time_2 like initialization.

Abhishek Kumar
  • 197
  • 2
  • 15
-2

If your code is in another file you need to patch where the import happens (lets call your file file1.py):

from file1 import get_report_month_key
import mock

@mock.patch("get_report_month_key.datetime.utcnow")
def test_get_report_month_key(mock_utcnow):
    mock_utcnow.return_value = "your value"
    assert get_report_month_key() == "your expected value"

Of course, I would wrap it with unittest framework.

Baryo
  • 314
  • 1
  • 9
  • @whats_done_js when I tried the above, I got ImportError: No module named get_report_month_key, and my function is indeed in say file1.py – Steven Yong May 07 '17 at 13:17
  • This fails for me with `TypeError: can't set attributes of built-in/extension type 'datetime.datetime'`. Further explanation here: https://solidgeargroup.com/mocking-the-time – Dirk Aug 21 '17 at 10:03
  • 1
    You cannot patch the attributes of built-ins. – David Gourde Aug 25 '17 at 15:48