
Published
Updated
A short post explaining how to mock `django.utils.timezone.now` in unit tests.
I recently had to implement a Django model that represents a competition with a start_time
field
and an end_time
field.
To determine whether the competition is active, I get the current time using
django.utils.timezone.now()
and check if it's within the time interval between start_time
and
end_time
.
So far so good, the method looks like this:
# models.py
from django.db import models
from django.utils import timezone
class Competition(models.Model):
start_time = models.DateTimeField()
end_time = models.DateTimeField()
def is_active(self) -> bool:
now = timezone.now()
return now >= self.start_time and now < self.end_time
When we run the code, timezone.now()
will return a datetime
object representing the current
time, and this is an issue because unit tests should be deterministic and always yield the same
results. We cannot write tests that will yield different results depending on the time they're run
at.
This is where mocking comes in: in our unit tests we will mock timezone.now()
so that it returns
the same datetime
objects every time.
# test_models.py
from datetime import datetime, timezone
from unittest.mock import patch
from .models import Competition
# `timezone.now` will now always return the value we want
@patch(
"django.utils.timezone.now",
lambda: datetime(2022, 1, 15, tzinfo=timezone.utc),
)
def test_Competition_is_active():
competition = Competition(
start_time=datetime(2021, 12, 1, tzinfo=timezone.utc),
end_time=datetime(2022, 1, 31, tzinfo=timezone.utc),
)
assert competition.is_active() is True
competition = Competition(
start_time=datetime(2021, 12, 1, tzinfo=timezone.utc),
end_time=datetime(2021, 1, 31, tzinfo=timezone.utc),
)
assert competition.is_active() is False
Now that we don't have to worry about the runtime value of timezone.now
, we can run our CI/CD
pipelines with the same results on every run, and avoid unexpected failures.