Testing Mastery

Testing Time-Dependent Code

March 20, 2026
5 min read
Featured image for blog post: Testing Time-Dependent Code

Your subscription expiration check passes today. Tomorrow it fails. Next month it fails differently.

Follow along with the code: iOS-Practice on GitHub

The Problem with Date()

class SubscriptionManagerBefore {
    func isSubscriptionValid(_ subscription: Subscription) -> Bool {
        // ❌ Can't control "now" in tests
        return subscription.expirationDate > Date()
    }

    func daysUntilExpiration(_ subscription: Subscription) -> Int {
        // ❌ Will give different results at different times
        let calendar = Calendar.current
        let components = calendar.dateComponents(
            [.day],
            from: Date(),
            to: subscription.expirationDate
        )
        return max(0, components.day ?? 0)
    }

    func shouldShowRenewalReminder(_ subscription: Subscription) -> Bool {
        // ❌ Can't test the 7-day threshold
        let daysLeft = daysUntilExpiration(subscription)
        return daysLeft <= 7 && daysLeft > 0
    }
}

Want to try it yourself? In DateTimeExerciseView.swift, delete everything from line 96 onwards (the // MARK: - AFTER section) and try to refactor SubscriptionManagerBefore to be testable. Your goal: make all tests in DateTimeExerciseTests.swift pass.

Why These Tests Fail

Tests using Date() directly are inherently flaky:

  • Midnight boundary: Test passes at 11:59 PM, fails at 12:01 AM
  • Month/year boundaries: Edge cases around Feb 28/29, Dec 31
  • Time zones: Runs fine locally, fails in CI (different timezone)
  • Race conditions: Test runs slower one day, date changes mid-test

You can't test "expires in 7 days" logic if you can't control what "now" means.

The Pattern: Inject Time

Instead of calling Date() directly, inject a date provider:

protocol DateProviding {
    var now: Date { get }
}

struct SystemDateProvider: DateProviding {
    var now: Date { Date() }
}

Then use it in your code:

class SubscriptionManager {
    private let dateProvider: DateProviding

    init(dateProvider: DateProviding = SystemDateProvider()) {
        self.dateProvider = dateProvider
    }

    func isSubscriptionValid(_ subscription: Subscription) -> Bool {
        return subscription.expirationDate > dateProvider.now
    }

    func daysUntilExpiration(_ subscription: Subscription) -> Int {
        let calendar = Calendar.current
        let components = calendar.dateComponents(
            [.day],
            from: calendar.startOfDay(for: dateProvider.now),
            to: calendar.startOfDay(for: subscription.expirationDate)
        )
        return max(0, components.day ?? 0)
    }
}

Production code uses SystemDateProvider() by default. Tests inject a mock.

Common Time-Dependent Bugs

BugSymptomRoot Cause
Flaky expiration testsPass sometimes, fail other timesUsing Date() directly
"Off by one" errors6 days shows as 7Not using startOfDay
Timezone issuesCI failuresHardcoded timezone assumptions
Leap year bugsFeb 29 edge casesNot testing boundary dates

The fix is always the same: make time a dependency you control.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch