pythonmockingpytestpytest-mock

Mocking datetime.now using pytest-mock


I have a function func_to_mock in module module_to_mock.py. My unit test is located in test_func_to_mock.py

I am trying to mock datetime.datetime.now, however, I am struggling. I get the error TypeError: cannot set 'now' attribute of immutable type 'datetime.datetime'.

What am I missing here?

module_to_mock.py:

# module_to_mock.py

import datetime


def func_to_mock() -> datetime.datetime:
    return datetime.datetime.now()

My unit test:

# test_func_to_mock.py

import datetime

from pytest_mock import MockFixture

import port_bt.module_to_mock


def test_func_to_mock(mocker: MockFixture) -> None:
    # This will error with:
    # TypeError: cannot set 'now' attribute of immutable type 'datetime.datetime'
    mocked_date = mocker.patch(
        "datetime.datetime.now", return_value=datetime.datetime(2023, 1, 31, 0, 0, 0)
    )

    assert port_bt.module_to_mock.func_to_mock() == mocked_date

Solution

  • As you've seen, you cannot patch Python standard library classes that are implemented in C (according to these unittest.mock docs). However, you can patch the datetime package like so:

    test_func_to_mock.py

    import datetime
    from pytest_mock import MockFixture
    
    import module_to_mock
    
    
    def test_func_to_mock(mocker: MockFixture) -> None:
    
        mocked_datetime = mocker.patch(
            "module_to_mock.datetime",  # 1
        )
        jan_31 = datetime.datetime(2023, 1, 31, 0, 0, 0)
        mocked_datetime.datetime.now.return_value = jan_31
    
        assert module_to_mock.func_to_mock() == jan_31 # 2
    

    Note that

    1. It is often better to patch the module under test rather than the datetime package directly. See also where to patch.
    2. We expect the date time value to be returned, not the mock object (which is what your assertion expects, I think).

    In response to the question about fixtures, I would say that sounds like a very good idea. You should be able to yield the mock object from a fixture so that you don't have to patch() in every test function:

    test_func_with_fixture.py

    import datetime
    from typing import Final, Generator
    from unittest.mock import MagicMock
    
    import pytest
    from pytest_mock import MockFixture
    
    import module_to_mock
    
    JAN_31: Final[datetime.datetime] = datetime.datetime(2023, 1, 31, 0, 0, 0)
    
    @pytest.fixture
    def datetime_fixture(mocker: MockFixture) -> Generator[MagicMock, None, None]:
        mocked_datetime = mocker.patch(
            "module_to_mock.datetime",
        )
        mocked_datetime.datetime.now.return_value = JAN_31
        yield mocked_datetime
        
    
    def test_func_to_mock(datetime_fixture: MagicMock) -> None:
        assert module_to_mock.func_to_mock() == JAN_31