Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Controlling date and time in tests
#1
Often, the programs we write and systems we build have to do something based on date and/or time. One example could be a library application that computes fines for overdue items and the amount of the fine varies based on how overdue the item is. Another example could be a system for publishing news items where users want the ability to add their articles to the system but only have them made visible on a certain day and time.

Automated testing (for example, unit testing) is an important part of software development. Manual testing is time consuming and unreliable, so writing test code to check that our system behaves the way we expect gives us a repeatable and reliable to do that. This is important because we have to change our software a lot: to add functionality, to fix bugs, for example and we want to know that everything still works.

Writing tests for code that makes use of date and time can be problematic. Here we look at the problems and explain an important concept in software development that allows us to make such code easier to test.

All the code for this tutorial can be found on GitHub: https://github.com/ndc85430/controlling-...e-in-tests. The repo includes a small application as well as tests and here, I make reference to specific commits.

The problem

We'll use the library application mentioned above. Check out the code at commit 4df9991 in the repository above.

Let's say that in this library, the fine is 1 (choose your favourite currency!) for every day the item is overdue. We might write a function to calculate the fine as follows:

fines.py:
from datetime import date


def fine_for(item):
    today = date.today()
    days_overdue = (today - item.due_date).days

    fine = 1 * days_overdue if days_overdue >= 1 else 0

    return fine
We might then write a unit test for this function to check we get the correct fine when the item is overdue:

test_fine_for.py:
    def test_the_fine_is_one_for_each_day_overdue(self):
        item = LoanedItem(
            title="Working Effectively with Legacy Code",
            author="Michael Feathers",
            due_date=date(year=2022, month=9, day=15)
        )

        fine = fine_for(item)

        self.assertEqual(fine, 2)
When I wrote this version, it was September 17th 2022 and since the item had a due date of September 15th 2022, we'd expect to get a fine of 2 since it is 2 days overdue. Of course, the test passed. By the time you read this, September 17th 2022 will have passed. Now the test will begin to fail:

Output:
FAIL: test_the_fine_is_one_for_each_day_overdue (test_fine_for.TestFineFor) ---------------------------------------------------------------------- Traceback (most recent call last): File "/home/user/controlling-date-and-time-in-tests/test_fine_for.py", line 40, in test_the_fine_is_one_for_each_day_overdue self.assertEqual(fine, 2) AssertionError: 3 != 2
I ran this on September 18th 2022 and while I expected the fine to be 2 in the test, the calculated fine is now 3 since there are 3 days between September 15th and 18th. You can see that this will basically be a problem every day now!

Introducing dependency injection

So, what can we do about this? Some ideas:

- Change the tests every day with the values we expect? This is a hassle and likely to be quite time consuming if we have lots of places where we need to do this.
- Change the date on the system when we want to run the tests? This doesn't sound like a good option because it might break things on the system, for a start.

What we'd really like is to be able to control the date that the function sees in the tests. If we can make the function always see a date of September 17th 2022 for the test above, it will pass regardless of what the actual date is. That's fine, because the application doesn't really care what the actual dates are, only the number of days between the due date and the date of today.

Let's look at the fine_for function again:

from datetime import date


def fine_for(item):
    today = date.today()
    days_overdue = (today - item.due_date).days

    fine = 1 * days_overdue if days_overdue >= 1 else 0

    return fine
This will only ever calculate today from the current date on the system (i.e. by calling date.today()). What do we do when we want to allow a function to handle different values? We pass them in as parameters!

The code for this section can be found at commit c79d8f2.

So, let's pass something in to the function from which we can find the date. People look at a calendar when they want to know what date it is, so our function might now look like this:

fines.py
def fine_for(item, calendar):
    today = calendar.today()
    days_overdue = (today - item.due_date).days

    fine = 1 * days_overdue if days_overdue >= 1 else 0

    return fine
and in our tests, we need a calendar. We can make one that when asked for what "today" is, it always responds with the same value and pass it into the function:

test_fine_for.py:
class FixedCalendar:
    def today(self):
        return date(year=2022, month=9, day=17)


calendar = FixedCalendar()


class TestFineFor(unittest.TestCase):
    def test_the_fine_is_one_for_each_day_overdue(self):
        item = LoanedItem(
            title="Working Effectively with Legacy Code",
            author="Michael Feathers",
            due_date=date(year=2022, month=9, day=15)
        )

        fine = fine_for(item, calendar)

        self.assertEqual(fine, 2)
Passing in a component your function needs to do its job is a technique known as dependency injection. We say that the calendar is a dependency of the fine_for function. The calendar we've created in the test that always returns a fixed date is a type of test double - you'll see different words like "mock", "stub" or "fake" used to describe various kinds of test doubles. We say we're injecting a test double into the fine_for function. One thing to mention is that you'll usually see dependency injection referred to in the context of classes - where dependencies are passed into a class' __init__ method, for example. Since I didn't really need a class here, I didn't use one, but the idea is the same: pass in the dependencies as parameters so that you can substitute them with test doubles to make testing easier.

This idea of dependency injection is useful not only in this situation, but in others too. Imagine that we have a system that has to go and fetch some data from another system, over the network (for example by using a REST API). There could be network problems or the other system could go down, causing problems for testing our code that calls that system. Instead, we use test doubles and inject them so that we can test regardless.

Earlier on, I mentioned that the repo contains an application as well as the tests. In that code, we need a calendar that does get the date from the system:

calendar.py:
from datetime import date


class Calendar:
    def today(self):
        return date.today()
and in the application, we create an instance of that and pass it to the fine_for function:

fine_application.py:
# Code above omitted for brevity
calendar = Calendar()

print("Title,Due date,Fine")

for item in items:
    fine = fine_for(item, calendar)

    print(f"{item.title},{item.due_date},{fine}")
Extras

There are a couple extra bits I wanted to mention.

1. Since I figured most readers would be familiar with calling methods on objects, I introduced a calendar in the fine_for function that has a method called today. There are two classes that fit the bill here - Calendar in the main code and FixedCalendar in the test code. These classes really don't do much for us other than contain the today methods. We have to instantiate them, even though we'd never need more than one instance of each of them. Having the classes is just extra, unnecessary noise! All the fine_for function really needs is something to ask what today's date is. If the classes aren't providing any value, can't we just have a function that does that? Why yes!

The code shown here can be found at commit 64f6eba.

fines.py:
def fine_for(item, today):
    days_overdue = (today() - item.due_date).days

    fine = 1 * days_overdue if days_overdue >= 1 else 0

    return fine
today is clearly a function, because it's being called on the first line of fine_for. Where does it come from? Well, it's passed in as the second parameter to fine_for. In Python, as in many other languages these days, functions are first class, meaning that they can be assigned to variables or passed to functions in the same way as other values (like integers or strings, say).

The code in the tests and the application is now simpler. First the tests:

test_fine_for.py:
def today():
    return date(year=2022, month=9, day=17)


class TestFineFor(unittest.TestCase):
    def test_the_fine_is_one_for_each_day_overdue(self):
        item = LoanedItem(
            title="Working Effectively with Legacy Code",
            author="Michael Feathers",
            due_date=date(year=2022, month=9, day=15)
        )

        fine = fine_for(item, today)

        self.assertEqual(fine, 2)
The FixedCalendar class we had before has just been replaced with a function called today that returns the same date and is passed in to fine_for on line 13 (on this forum, line numbers in the repo are different as I've omitted some code). Note the lack of parentheses after today on line 13. Including the parentheses calls the function to produce its return value. That's not what we want to do. We want to pass the function object itself to fine_for so that it can be called there, so we omit the parentheses.

It's the same kind of thing in the application:

fine_application.py:
from datetime import date

# Code in between omitted for brevity.

for item in items:
    fine = fine_for(item, date.today)

    print(f"{item.title},{item.due_date},{fine}")
Here, we don't even need to define our own function, because the today method on the date class is exactly the function we want.

2. At the beginning of this tutorial, I mentioned the importance of automated testing of our software. However, if you've followed the various versions of the code up to now you might have noticed that there weren't any tests for the fine application, only for the fine_for function. This was on purpose to focus on the idea of dependency injection.

fine_application.py as originally written wasn't easy to write tests for - there was nothing you could call (like a function) in test code. Also, the application prints to the console and it's hard to capture the output written there in a test.

The code shown here can be found at commit 0280699.

To deal with these problems, we do two things. First, we introduce a FineApplication class, that has a run method to, well, run the application. Next, we use dependency injection to allow us to pass a function in to write the lines of output to a string for the tests (as well as the function to get today's date):

fine_application.py:
class FineApplication:
    def __init__(self, get_items, today, write_line):
        self.get_items = get_items
        self.today = today
        self.write_line = write_line

    def run(self):
        items = self.get_items()

        self.write_line(f"Today is: {self.today()}")
        self.write_line("")
        self.write_line("Fines due:")
        self.write_line("")

        self.write_line("Title,Due date,Fine")

        for item in items:
            fine = fine_for(item, self.today)

            self.write_line(f"{item.title},{item.due_date},{fine}")
test_fine_application.py:
# Imports omitted for brevity
def get_items():
    return [
        LoanedItem(
            title="Growing Object Oriented Software Guided By Tests",
            author="Nat Pryce and Steve Freeman",
            due_date=date(year=2022, month=10, day=14)
        )
    ]


def today():
    return date(year=2022, month=10, day=1)


def write_line_to_string(line):
    write_line_to_string._output += f"{line}\n"

write_line_to_string._output = ""

class TestFineApplication(unittest.TestCase):
    def test_it_produces_a_report_of_the_fines_due(self):
        FineApplication(get_items, today, write_line_to_string).run()

        self.assertEqual(
            write_line_to_string._output,
            """Today is: 2022-10-01
            |
            |Fines due:
            |
            |Title,Due date,Fine
            |Growing Object Oriented Software Guided By Tests,2022-10-14,0
            |""".replace("            |", "")
        )
You'll note that I also pass in a get_items function as a dependency. I want to be able to see the items in the test code (otherwise it's not clear why the report produced contains the data that it does) and as I mentioned before, in a real system such data might have to come from a separate system.

I refer to the test of the application as a functional test, because it tests the functionality of the application as a whole rather than a unit test, which tests an individual part (like a function). Other people might use other terms, like acceptance test, integration test or end to end test.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Getting stared with unit tests in Python jdjeffers 0 2,236 Oct-15-2018, 03:40 PM
Last Post: jdjeffers

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020