Object-Oriented C# · Lesson 16

Manual dependency injection for repeatable tests

Pass a controlled source of time instead of reading a global clock inside a calculation.

How it works

Dependency injection means supplying an object's collaborator from outside rather than constructing or locating it invisibly inside the object. The central idea is ordinary parameter passing. A framework can automate construction in a large application, but a small program can express the same design clearly with constructors and interfaces.

A clock is a useful example because direct reads of the current time make an otherwise simple rule change from one run to another. Passing an IClock lets a test provide a fixed instant. The business object still decides whether a deadline has passed; the clock supplies only the current instant. This separates policy from an environmental observation without turning the example into a complete scheduling system.

Walk through the example. FixedClock returns the instant supplied at construction. Deadline receives that clock and compares its DueAt value with Now. Two fixed clocks represent one instant before the deadline and the deadline itself. The IsDue property is false before the boundary and true at the boundary, making the inclusive comparison visible and repeatable in the output.

Watch the boundary. An interface is not required for every value in a program. Introduce the boundary where variation or control is useful, rather than wrapping simple arithmetic in unnecessary layers. Also avoid a mutable global test clock shared between unrelated tests; constructor-supplied instances make each test's assumptions local and prevent accidental interference between scenarios.

Practice independently. Write an expiration rule with a precisely documented equality boundary and test before, exactly at, and after expiration using fixed clocks. Add a real clock implementation that reads DateTimeOffset.UtcNow for ordinary use, but keep the prepared tests independent of it. Explain which object owns the policy and which merely supplies the observation, and why that distinction keeps the tests stable.

A working example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

var due = new DateTimeOffset(2026, 1, 2, 12, 0, 0, TimeSpan.Zero);
Console.WriteLine(new Deadline(due, new FixedClock(due.AddMinutes(-1))).IsDue);
Console.WriteLine(new Deadline(due, new FixedClock(due)).IsDue);
interface IClock { DateTimeOffset Now { get; } }
sealed class FixedClock : IClock
{
    public DateTimeOffset Now { get; }
    public FixedClock(DateTimeOffset now) { Now = now; }
}
sealed class Deadline
{
    private readonly IClock clock;
    public DateTimeOffset DueAt { get; }
    public Deadline(DateTimeOffset dueAt, IClock clock) { DueAt = dueAt; this.clock = clock; }
    public bool IsDue => clock.Now >= DueAt;
}

Expected output

False
True
Try this
Pass the observation source explicitly so tests can control time without changing the business rule.