Mateusz Stompór
Mateusz Stompór

Reputation: 489

Efficient and testable structs

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

Answers (1)

Shadowrun
Shadowrun

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

Related Questions