2

Trying to get mocking to work for datetime.date.today() I finally got it working, after reading this answer and this documentation. I did not get my original version to work though, and I was wondering if anyone could help me understand?

My original implementation was to call dt.date.today() after importing datetime like this import datetime as dt. This can be seen in mymodule.today_string_dt_date. The testing was done by mocking mymodule.dt.date (not possible to mock mymodule.dt.date.today directly as it is a builtin), and setting mock.today.return_value = dt.date(2016,1,10) after importing datetime in the same way as in mymodule.py (import datetime as dt)

I then implemented a version that was importing date directly (from datetime import date) and created tests that was setting the mock.today.return_value to both dt.date(2016,1,10) and date(2016,1,10) for both versions. All test versions but my original one are passing, as can be seen in the output from running py.test -v

It looks like using the same import scheme (import datetime as dt) for both module and test and using that to create the mock return value (as in the test test_today_string_dt_date) ruins the mock.

mymodule.py

import datetime as dt
from datetime import date


def today_string_dt_date():
    return dt.date.today().strftime('%Y-%m-%d')


def today_string_date():
    return date.today().strftime('%Y-%m-%d')


if __name__ == '__main__':
    print(today_string_dt_date())

test_mymodule.py

import datetime as dt
from datetime import date
from unittest.mock import patch
import unittest

import mymodule


class MyTest(unittest.TestCase):
    @patch('mymodule.dt.date')
    def test_today_string_dt_date(self, mock_date):
        mock_date.today.return_value = dt.date(2016, 1, 10)
        assert mymodule.today_string_dt_date() == '2016-01-10'

    @patch('mymodule.dt.date')
    def test_today_string_dt_date_alt(self, mock_date):
        mock_date.today.return_value = date(2016, 1, 10)
        assert mymodule.today_string_dt_date() == '2016-01-10'

    @patch('mymodule.date')
    def test_today_string_date(self, mock_date):
        mock_date.today.return_value = dt.date(2016, 1, 10)
        assert mymodule.today_string_date() == '2016-01-10'

    @patch('mymodule.date')
    def test_today_string_date_alt(self, mock_date):
        mock_date.today.return_value = date(2016, 1, 10)
        assert mymodule.today_string_date() == '2016-01-10'

py.test output

=========================== test session starts ===========================
platform darwin -- Python 3.5.1, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
collected 4 items 

test_mymodule.py::MyTest::test_today_string_date PASSED
test_mymodule.py::MyTest::test_today_string_date_alt PASSED
test_mymodule.py::MyTest::test_today_string_dt_date FAILED
test_mymodule.py::MyTest::test_today_string_dt_date_alt PASSED

================================ FAILURES =================================
____________________ MyTest.test_today_string_dt_date _____________________

self = <test_mymodule.MyTest testMethod=test_today_string_dt_date>
mock_date = <MagicMock name='date' id='4364815888'>

    @patch('mymodule.dt.date')
    def test_today_string_dt_date(self, mock_date):
        mock_date.today.return_value = dt.date(2016, 1, 10)    
>       assert mymodule.today_string_dt_date() == '2016-01-10'
E       AssertionError: assert <MagicMock name='date().strftime()'     id='4353329528'> == '2016-01-10'
E        +  where <MagicMock name='date().strftime()' id='4353329528'> = <function today_string_dt_date at 0x10429ed90>()
E        +    where <function today_string_dt_date at 0x10429ed90> = mymodule.today_string_dt_date

test_mymodule.py:14: AssertionError
=================== 1 failed, 3 passed in 0.05 seconds ====================
Community
  • 1
  • 1
jmbond
  • 88
  • 1
  • 6
  • One thing I like to do to aid testing is to make time-dependent functions take an optional argument. For example: `def some_function(dt=None): dt = datetime.now() if dt is None else dt` - this lets you skip mocking altogether. – bbayles Jan 18 '16 at 15:09
  • Yeah, I see that as a great alternative. Thanks! My original code is bit more involved than the trimmed down version shown here and the function was called within another function. Then I would have to add `dt=None` to the calling function also? Not so sure if I like that. What if there are several of these cases? Could be I'm designing badly to begin with though.. – jmbond Jan 18 '16 at 15:40
  • With a keyword argument you could leave the callers alone! – bbayles Jan 18 '16 at 15:54

1 Answers1

0

It is just because by @patch('mymodule.dt.date') you are patching the original date reference in datetime module. In this context dt.date(2016, 1, 10) is a MagicMock where the name is date().

From this you have today_string_dt_date() will return a date().strftime() MagicMock.

I admit that is a little bit twisted and it is for this kind of things that I try avoid complicated paths in patch: my rules are

  1. Try to patch just the root reference.
  2. If you cannot follow 1 (module with from a import b) just patch the module reference and never browse from it.
Michele d'Amico
  • 22,111
  • 8
  • 69
  • 76
  • So by using `@patch('mymodule.dt.date')`, I patch `datetime.date` globally? That is really confusing.. Thank you for the advice though! At least I learned to be a bit careful with complicated paths in patch. – jmbond Jan 19 '16 at 16:28
  • I recommend this great read for python mocking gotchas: http://alexmarandon.com/articles/python_mock_gotchas/ – krakover Jan 13 '17 at 22:45