Mocking time in Python

python mocking

A couple of years ago, I dealt with a network device that didn’t add a year to the string that described the last time an interface flapped, e.g. THU OCT 02 14:07:47. We needed the exact date to figure out which interfaces had been down for X amount of days. I wrote a script that completed the datetime object by calculating the most recent year an incomplete datetime string occurred on a specific weekday.

def parse_date_from_incomplete_string(incomplete_dt: str) -> datetime:
    current_year = datetime.today().year
    search_range = range(current_year, current_year - 28, -1)
    candidate_dates = _find_possible_dates(search_range, incomplete_dt)

    for dt in candidate_dates:
        if _weekdays_match(dt, incomplete_dt):
            return dt
    raise ValueError("Impossible date")

I also wrote a test in case I was going to modify the script in the future.

@pytest.mark.parametrize(
    "partial_date, expected",
    [
        ("THU OCT 02 14:07:47", datetime(2014, 10, 2, 14, 7, 47)),
        ("MON MAR 02 11:40:47", datetime(2020, 3, 2, 11, 40, 47)),
        ("TUE JAN 26 00:16:32", datetime(2016, 1, 26, 0, 16, 32)),
        ("WED JUN 28 17:03:26", datetime(2017, 6, 28, 17, 3, 26)),
        ("FRI NOV 30 10:55:55", datetime(2018, 11, 30, 10, 55, 55)),
        ("MON SEP 09 14:58:19", datetime(2019, 9, 9, 14, 58, 19)),
        ("MON FEB 29 13:37:00", datetime(2016, 2, 29, 13, 37, 0)),
    ],
)
def test_find_date_from_incomplete_string(partial_date, expected):
    assert parse_date_from_incomplete_string(partial_date) == expected

But recently one of the tests started failing:

    def test_find_date_from_incomplete_string(partial_date, expected):
>       assert parse_date_from_incomplete_string(partial_date) == expected
E       assert datetime.datetime(2021, 1, 26, 0, 16, 32) == datetime.datetime(2016, 1, 26, 0, 16, 32)
E         +datetime.datetime(2021, 1, 26, 0, 16, 32)
E         -datetime.datetime(2016, 1, 26, 0, 16, 32)

The script found a more recent date than the one pytest expected, because I used datetime.today().year inside the parse_date_from_incomplete_string function, and that value will obviously change as time moves forward.

FreezeGun library—no mocking or patching needed

There’s a popular and well-documented package called FreezeGun; it can freeze time for parts of your code.

$ pip install freezegun

I added the @freeze_time decorator to my test function and gave it the argument 2020-03-03. From that point on, datetime.today().year returns 2020 in perpetuity when it’s called by pytest.

@freeze_time("2020-03-03")
@pytest.mark.parametrize(
    "partial_date, expected",
    [
        ("THU OCT 02 14:07:47", datetime(2014, 10, 2, 14, 7, 47)),
        ("MON MAR 02 11:40:47", datetime(2020, 3, 2, 11, 40, 47)),
        ("TUE JAN 26 00:16:32", datetime(2016, 1, 26, 0, 16, 32)),
        ("WED JUN 28 17:03:26", datetime(2017, 6, 28, 17, 3, 26)),
        ("FRI NOV 30 10:55:55", datetime(2018, 11, 30, 10, 55, 55)),
        ("MON SEP 09 14:58:19", datetime(2019, 9, 9, 14, 58, 19)),
        ("MON FEB 29 13:37:00", datetime(2016, 2, 29, 13, 37, 0)),
    ],
)
def test_find_date_from_incomplete_string(partial_date, expected):
    assert parse_date_from_incomplete_string(partial_date) == expected

And now the tests pass again:

test_parse_incomplete_date.py::test_find_date_from_incomplete_string[THU OCT 02 14:07:47-expected0] PASSED
...
test_parse_incomplete_date.py::test_find_date_from_incomplete_string[MON FEB 29 13:37:00-expected6] PASSED

Could you solve the problem without an external library?

Yes, you could change the API of your function and use dependency injection.

rom datetime import datetime
from datetime import timedelta


def yesterday(asof: datetime) -> datetime:
    return asof - timedelta(days=1)

If you provide today’s date as a function argument you remove the function’s internal dependency on datetime.today().

from datetime import datetime

from yesterday_asof import yesterday


def test_yesterday():
    assert yesterday(asof=datetime(1983, 1, 1)) == datetime(1982, 12, 31)