Testing Date Span in Swift: Controlling the current date & time | iOS Lead Essentials Community Q&A
In this episode, we reply to a question we received in the private iOS Lead Essentials Slack community.
Valentin asked how to test a function that generates a date span without duplicating the production code in the test side.
To illustrate, imagine you need to test a function that returns the start and end of the current day (GMT).
In the production side, you can determine the start of the day using a Calendar instance:
func todayDateSpan() -> (start: Date, end: Date) {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(abbreviation: "GMT")!
let startOfToday = calendar.startOfDay(for: Date())
let endOfToday = …
return (startOfToday, endOfToday)
}
But you also need to determine the 'start of today' in the test side to create the values to use in the assertion. So, how can you test this function without duplicating this exact code?
func test_startOfToday() {
// duplicate
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(abbreviation: "GMT")!
let startOfToday = calendar.startOfDay(for: Date())
let result = todayDateSpan()
XCTAssertEqual(result.start, startOfToday)
}
The problem is: the call to Date()
is an impure operation because every time you call Date()
, you get a new value (time never stops!).
And impure functions make test results unpredictable.
(A "pure" function is guaranteed to always return the same output for the same input. An "impure" function is not.)
Thus, it's common to see tests that are a mere copy/paste of the production code. Especially when the test is written after.
But duplicating code in the test side is not a good strategy. For example, if there's a bug in production, the test won't review it.
There's a better way: You can replace impure/unpredictable dependencies with pure/predictable ones.
In this case, you need to make the current date predictable.
One way of achieving it is to inject the current Date
value, so you don't need to call the impure Date()
function inside the method.
func todayDateSpan(currentDate: Date) -> ...
Now you can test the function with a fixed 'current date' to produce predictable results:
func test_startOfToday() {
let today = Date(timeIntervalSince1970: 1590242591) // 23 May 2020 14:03:11 GMT
let startOfToday = Date(timeIntervalSince1970: 1590192000) // 23 May 2020 00:00:00 GMT
let result = todayDateSpan(currentDate: today)
XCTAssertEqual(result.start, startOfToday)
}
If needed, you can give it a default value (e.g., to make sure you don't break current clients of the API):
func todayDateSpan(currentDate: Date = Date()) -> ...
If an operation needs to continuously get the current date, you can inject a closure that returns a Date
instance instead:
func todayDateSpan(currentDate: () -> Date = Date.init) -> ...
You can use the same technique to reliably test code that relies on any environment dependencies and impure operations. You can inject Date, Calendar, TimeZone, Databases, Network…
Subscribe to our YouTube channel and don't miss out on new episodes.
References
- Timestamp converter used in this episode
- How to Build iOS Apps with Swift, TDD & Clean Architecture YouTube series
- Join us in the Essential Developer Academy