Reputation: 489
I'm trying to find a way to define a struct so that it holds non-pointer types inside it and preserve ability to test it easily. I don't want to use protocols since instead of having the actual data inside the struct pointers would be held. If I decided to use ptr backing types I would implement an example piece of code in such a way
protocol TimeGetable {
var currentTime: Date { get }
}
struct Chronometer {
private let timeGetter: TimeGetable
private var startTime: Date
init(timeGetter: TimeGetable) {
self.timeGetter = timeGetter
startTime = timeGetter.currentTime
}
mutating func reset() {
startTime = timeGetter.currentTime
}
func elapsedTime() -> TimeInterval {
timeGetter.currentTime.timeIntervalSince(startTime)
}
}
struct RealGetter: TimeGetable {
var currentTime: Date { Date() }
}
struct FakeGetter: TimeGetable {
var currentTime: Date {
// Do some magic here to ease testing
// Return the same value each time as an example
return Date.distantPast
}
}
In this approach no matter how big the class/struct implementing TimeGetable
would be, size of Chronometer stays the same.
MemoryLayout<Chronometer>.size // 48 bytes
If I decided to not test my code at all I would define the Chronometer
in this way
struct Chronometer {
private var startTime: Date
init(timeGetter: TimeGetable) {
startTime = Date()
}
mutating func reset() {
startTime = Date()
}
func elapsedTime() -> TimeInterval {
Date().currentTime.timeIntervalSince(startTime)
}
}
In this case if Date
type size raise Chronometer rises as well since it holds non-reference type.
It's good for performance and avoiding cache misses.
The only hack which comes to my mind is to do something as follows
#if TESTS
public typealias DateType = DateMock
#else
public typealias DateType = Date
#endif
struct Chronometer {
private var startTime: DateType
init(timeGetter: TimeGetable) {
startTime = DateType()
}
mutating func reset() {
startTime = DateType()
}
func elapsedTime() -> TimeInterval {
DateType().currentTime.timeIntervalSince(startTime)
}
}
I would define TESTS
flag in the target with tests and nothing in the production code.
It seems to be rather an ugly solution. On top of that implies having mentions of classes/structs only used and defined for tests purposes in my production code.
Is there a better way?
Upvotes: 0
Views: 54
Reputation: 3867
Perhaps you could consider making a struct something like this:
struct Chronometer {
private let timeGetter: () -> Date
private var startTime: Date
init(timeGetter: @escaping () -> Date = Date.init) {
self.timeGetter = timeGetter
startTime = timeGetter()
}
mutating func reset() {
startTime = timeGetter()
}
func elapsedTime() -> TimeInterval {
timeGetter().timeIntervalSince(startTime)
}
}
That's trivial to mock/stub. And you can then call these functions in your struct's init to make the actual Date's that you care about. In the test case that would be some fixed date function, in live, the normal Date.init returning current date/time.
Upvotes: 1