software-design/dependency-injection-why-a-test-passes-at-9am-and-fails-at-3pm.md

Dependency injection: why a test passes at 9am and fails at 3pm

Building software · 5 min read ·

A test that passes in the morning and fails in the afternoon, with no change to the code, is a puzzle many beginners meet. The cause is often a function that fetches its own clock, or its own email service, instead of being given one.

Run the same two tests many times in the page and watch one flicker between green and red while the other never changes. Then see the small fix, called dependency injection, in short Python examples you can run yourself.

Dark card showing the value hour = 9 being passed into a function greeting(hour), with a row of green squares for passing tests

You write a function, and you write a test for it. The test passes. You run it again in the afternoon and it fails, even though you changed nothing. Where did the failure come from?

Often the function is reaching out and grabbing something by itself: the current time, a random number, a database, the internet. This article is about the simple fix that fixes it. Programmers call it dependency injection, which is a grand name for a small idea: hand the function what it needs, instead of letting it fetch things on its own.

A function that fetches its own clock

Here is a small Python function. A dependency is anything a piece of code needs in order to do its job. This function has one: the clock.

from datetime import datetime

def greeting():
    hour = datetime.now().hour
    if hour < 12:
        return "Good morning"
    return "Good afternoon"

print(greeting())   # "Good morning" before noon, "Good afternoon" after

The function decides for itself where the time comes from: it calls datetime.now(), which asks the computer for the real time. Now imagine the test:

assert greeting() == "Good morning"

Run before noon, this passes. Run after noon, it fails. The test is checking the function and the time you happened to run it. A test whose result changes when the code did not change is called flaky, and flaky tests are expensive: people stop trusting them.

Hand it in instead

The fix is to move the decision out of the function. Instead of fetching the hour, the function receives it as a parameter:

from datetime import datetime

def greeting(hour):
    if hour < 12:
        return "Good morning"
    return "Good afternoon"

# The test decides the hour, so the answer never changes:
assert greeting(9) == "Good morning"
assert greeting(15) == "Good afternoon"
print("tests passed")

# Real code passes in the real hour:
print(greeting(datetime.now().hour))

Running this prints tests passed, then a greeting that depends on when you run it. The function is the same logic as before. What changed is who is in charge of the clock: now the caller is. A test can say "pretend it is 9 o'clock" and get the same answer at 3 a.m. and at 3 p.m. Handing a dependency in from outside like this is dependency injection.

Try it: two tests, many times of day

Below, each button press runs both tests at a random hour of the day, as if you ran your test suite at different moments. The left test asks for the real clock. The right test chooses its own hour. (This page simulates the idea with a few lines of JavaScript that mirror the Python above; it does not run Python.)

After a few presses the left side shows a mix of green and red from the same code. The right side stays green every time, because nothing in it depends on when it runs.

It is not only about clocks

The same trouble appears whenever a function quietly reaches for something outside itself: sending an email, charging a card, reading a file, asking a web service. In a test you do not want a real email to go out to a real person. So you hand in a fake, a stand-in that looks like the real thing but just records what happened:

class FakeMailer:
    def __init__(self):
        self.sent = []
    def send(self, to, text):
        self.sent.append((to, text))

def welcome(user, mailer):
    mailer.send(user, "Welcome, " + user + "!")

fake = FakeMailer()
welcome("[email protected]", fake)
print(fake.sent)
# [('[email protected]', 'Welcome, [email protected]!')]

The function welcome does not know, or care, whether mailer is a real mail service or a fake. It only needs something with a send method. In real use you would pass the real mailer; in a test you pass the fake and then look inside it to check that the right message was "sent".

What to take from this

You do not need a framework or special tool for this. In Python, JavaScript and most other languages it is just a parameter. A few habits that follow:

  • If a test passes sometimes and fails sometimes, look for something the code fetches by itself: time, randomness, files, the network.
  • Give the function that thing as a parameter, and let the caller decide what to pass.
  • Keep the part that does the real fetching small and at the edge of your program, so the rest of the code is easy to test.
  • Do not add parameters for everything. A function that only does arithmetic needs no injection at all.

Next time a test feels unreliable, ask one question: what is this code grabbing that I do not control? Then let it be handed over instead.

$ ls software-design/

see all →
software-design/the-observer-pattern-how-a-shop-tells-many-functions-about-one-order-w.md The observer pattern: how a shop tells many functions about one order without knowing them A shop that must email a receipt, update stock and add loyalty points ends up with one function that knows about everything. Every new… 5 min read · 27 Oct software-design/refactoring-how-to-tidy-working-code-without-breaking-it.md Refactoring: how to tidy working code without breaking it Refactoring means changing how code is built on the inside without changing what it does. It sounds risky, but there is a simple routine… 6 min read · 24 Oct software-design/unit-tests-why-four-passing-tests-don-t-prove-a-function-works.md Unit tests: why four passing tests don't prove a function works Your function gave the right answer four times. Is it correct? Not necessarily: passing tests only show the bug isn't where you looked… 4 min read · 21 Oct software-design/idempotency-why-clicking-pay-twice-should-charge-you-once.md Idempotency: why clicking Pay twice should charge you once You tap Pay, the screen shows an error, you tap again, and your card is charged twice. Networks drop messages, so a phone often cannot tell… 6 min read · 18 Oct software-design/state-machines-why-an-order-can-t-be-delivered-before-it-s-paid.md State machines: why an order can't be delivered before it's paid An online order can be in the cart, paid, shipped, delivered or cancelled, but what stops the code from delivering an order nobody paid… 4 min read · 15 Oct software-design/pure-functions-why-the-same-call-can-give-two-different-answers.md Pure functions: why the same call can give two different answers Call a function twice with the same input and get two different answers? Python isn't broken. The function is doing something extra, called… 5 min read · 9 Oct