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
| Bug | Symptom | Root Cause |
|---|---|---|
| Flaky expiration tests | Pass sometimes, fail other times | Using Date() directly |
| "Off by one" errors | 6 days shows as 7 | Not using startOfDay |
| Timezone issues | CI failures | Hardcoded timezone assumptions |
| Leap year bugs | Feb 29 edge cases | Not testing boundary dates |
The fix is always the same: make time a dependency you control.