Last active
February 3, 2018 23:05
-
-
Save elm4ward/4d477f57550c571f00f203ed89c5d8c4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
let mark = Array(repeating: "-", count: 40).joined(separator: "") | |
func chapter(s: String, line: Int = #line) { print("\n", "// " + mark, "// \(s) - line: \(line)", "// " + mark, separator: "\n")} | |
func section(s: String, line: Int = #line) { print("", "~~ \(s)", separator: "\n")} | |
func assertEqual<A: Equatable>(_ a: A, _ b: A) { assert(a == b); print(a, "==", b) } | |
// ---------------------------------------------------------------------------------------------- | |
// It`s a generic type of world | |
// | |
// http://elm4ward.github.io/swift/generics/mocking/dependency/injection/2016/05/21/generic-dependency-mocking.html | |
// | |
// Mocking Dependencies with Generics | |
// | |
// 1. The Item | |
// 2. Solution A) ModelFixed | |
// 3. Solution B) ModelGlobal | |
// 4. - Adding a Webservice | |
// 5. - Adding a DateProvider | |
// 6. Solution C) ModelInjected | |
// 7. Environment | |
// 8. - Adding a Logging Service | |
// 9. - RealWorldEnvironment™ | |
// 10. - MockedWorldEnvironments | |
// 11. - Parallel Worlds | |
// 12. Solution D) ModelFor Environment | |
// 13. - Test all worlds | |
// ---------------------------------------------------------------------------------------------- | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 1. The Item | |
// ---------------------------------------------------------------------------------------------- | |
struct Item { | |
let date: Date | |
} | |
// prefill some basic items | |
let items = (1...10) | |
.map { Date().addingTimeInterval(Double($0) * 10) } | |
.map(Item.init(date:)) | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - Solution A) 2. ModelFixed | |
// ---------------------------------------------------------------------------------------------- | |
chapter(s: "2. Solution A) ModelFixed") | |
/// This model uses Date() directly. | |
struct ModelFixed { | |
let items: [Item] | |
var validItems: [Item] { | |
// how do you test that 10 minutes later? | |
let now = Date() | |
return items.filter { ($0.date > now ? $0.date : now) == $0.date } | |
} | |
} | |
let fixedModel = ModelFixed(items: items) | |
assertEqual(fixedModel.validItems.count, 10) | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 3. Solution B) ModelGlobal | |
// ---------------------------------------------------------------------------------------------- | |
chapter(s: "3. Solution B) ModelGlobal") | |
/// a global func and var to control the time | |
var timeShift: Date? = nil | |
func getNow() -> Date { | |
return timeShift ?? Date() | |
} | |
/// a closure to extecute code with a different `now` | |
func withShiftedTime(c: () -> ()) { | |
timeShift = Date().addingTimeInterval(60 * 60 * 24) | |
c() | |
timeShift = nil | |
} | |
/// This model uses the global func to get now | |
struct ModelGlobal { | |
let items: [Item] | |
var validItems: [Item] { | |
let now = getNow() | |
return items.filter { ($0.date > now ? $0.date : now) == $0.date } | |
} | |
} | |
section(s: "now") | |
let globalModel = ModelGlobal(items: items) | |
assertEqual(globalModel.validItems.count, 10) | |
section(s: "tomorrow") | |
timeShift = Date().addingTimeInterval(60 * 60 * 24) | |
assertEqual(globalModel.validItems.count, 0) | |
// reset | |
timeShift = nil | |
withShiftedTime { | |
section(s: "tomorrow - withShiftedTime") | |
assertEqual(globalModel.validItems.count, 0) | |
} | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 4. Adding a Webservice | |
// ---------------------------------------------------------------------------------------------- | |
/// This is our common protocol | |
protocol WebServiceType { | |
/// should be promise based ... you have to imagine | |
func checkValid(items: [Item], completion: (Bool) -> ()) | |
} | |
/// This is our real WebService | |
struct WebService: WebServiceType { | |
func checkValid(items: [Item], completion: (Bool) -> ()) { | |
// do stuff | |
} | |
} | |
/// This is our working WebService Mock | |
struct WebServiceMock: WebServiceType { | |
func checkValid(items: [Item], completion: (Bool) -> ()) { | |
completion(true) | |
} | |
} | |
/// This is our offline WebService Mock | |
struct WebServiceOfflineMock: WebServiceType { | |
func checkValid(items: [Item], completion: (Bool) -> ()) { | |
completion(false) | |
} | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 5. Adding a DateProvider | |
// ---------------------------------------------------------------------------------------------------- | |
protocol DateProvider { | |
var now: Date { get } | |
} | |
struct Now: DateProvider { | |
var now: Date { | |
return Date() | |
} | |
} | |
struct Tomorrow: DateProvider { | |
var now: Date { | |
return Date().addingTimeInterval(60 * 60 * 24) | |
} | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 6. Solution C) ModelInjected | |
// ---------------------------------------------------------------------------------------------------- | |
chapter(s: "Solution C) 6. ModelInjected") | |
struct ModelInjected { | |
let items: [Item] | |
let dateProvider: DateProvider | |
let webService: WebServiceType | |
init(items: [Item], dateProvider: DateProvider = Now(), webService: WebServiceType){ | |
self.items = items | |
self.dateProvider = dateProvider | |
self.webService = webService | |
} | |
var validItems: [Item] { | |
// how do you test that? | |
let now = dateProvider.now | |
return items.filter { ($0.date > now ? $0.date : now) == $0.date } | |
} | |
func sync(then callback: @escaping ([Item]) -> ()) { | |
webService.checkValid(items: validItems){ ok in | |
callback(ok ? self.validItems : []) | |
} | |
} | |
} | |
section(s: "now") | |
let modelInjected = ModelInjected(items: items, dateProvider: Now(), webService: WebServiceMock()) | |
assertEqual(modelInjected.validItems.count, 10) | |
modelInjected.sync { | |
section(s: "-> after server sync") | |
assertEqual($0.count, 10) | |
} | |
section(s: "tomorrow") | |
let modelInjectedTomorrow = ModelInjected(items: items, dateProvider: Tomorrow(), webService: WebServiceMock()) | |
assertEqual(modelInjectedTomorrow.validItems.count, 0) | |
modelInjectedTomorrow.sync { | |
section(s: "-> after server sync") | |
assertEqual($0.count, 0) | |
} | |
// ---------------------------------------------------------------------------------------------------- | |
// MARK: - 7. Environment | |
// ---------------------------------------------------------------------------------------------------- | |
// The outer world | |
protocol Environment { | |
static var now: Date { get } | |
static var webService: WebServiceType { get } | |
static var loggingService: LoggingServiceType { get } | |
} | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 8. Adding a Logging Service | |
// ---------------------------------------------------------------------------------------------- | |
protocol LoggingServiceType { | |
/// should be promise based ... you have to imagine | |
func log( message: @autoclosure () -> String) | |
} | |
/// This is our real LoggingService | |
struct LoggingService<Env: Environment>: LoggingServiceType { | |
func log( message: @autoclosure () -> String) { | |
print(Env.now, message()) | |
} | |
} | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 9. RealWorldEnvironment™ | |
// ---------------------------------------------------------------------------------------------- | |
protocol RealWorldEnvironment: Environment {} | |
extension RealWorldEnvironment { | |
static var now: Date { | |
return Date() | |
} | |
static var webService: WebServiceType { | |
return WebService() | |
} | |
} | |
/// the place to be | |
struct RealWorld: RealWorldEnvironment { | |
static var loggingService: LoggingServiceType { | |
return LoggingService<RealWorld>() | |
} | |
} | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 10. MockedWorldEnvironments | |
// ---------------------------------------------------------------------------------------------- | |
/// sunny -> Online | |
protocol MockedOnlineWorldEnvironment: Environment {} | |
/// stormy -> Offline | |
protocol MockedOfflineWorldEnvironment: Environment {} | |
/// the online base world | |
extension MockedOnlineWorldEnvironment { | |
static var webService: WebServiceType { | |
return WebServiceMock() | |
} | |
} | |
/// the offline base world | |
extension MockedOfflineWorldEnvironment { | |
static var webService: WebServiceType { | |
return WebServiceOfflineMock() | |
} | |
} | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 11. Parallel Worlds | |
// ---------------------------------------------------------------------------------------------- | |
struct WorldInTenSeconds: MockedOnlineWorldEnvironment { | |
static var now: Date { | |
return Date().addingTimeInterval(10) | |
} | |
static var loggingService: LoggingServiceType { | |
return LoggingService<WorldInTenSeconds>() | |
} | |
} | |
struct WorldYesterday: MockedOnlineWorldEnvironment { | |
static var now: Date { | |
return Date().addingTimeInterval(-60 * 60 * 24) | |
} | |
static var loggingService: LoggingServiceType { | |
return LoggingService<WorldYesterday>() | |
} | |
} | |
struct WorldTomorrow: MockedOnlineWorldEnvironment { | |
static var now: Date { | |
return Date().addingTimeInterval(60 * 60 * 24) | |
} | |
static var loggingService: LoggingServiceType { | |
return LoggingService<WorldTomorrow>() | |
} | |
} | |
struct WorldYesterdayOffline: MockedOfflineWorldEnvironment { | |
static var now: Date { | |
return Date().addingTimeInterval(-60 * 60 * 24) | |
} | |
static var loggingService: LoggingServiceType { | |
return LoggingService<WorldYesterdayOffline>() | |
} | |
} | |
// ---------------------------------------------------------------------------------------------- | |
// MARK: - 12. Solution D) ModelFor Environment | |
// ---------------------------------------------------------------------------------------------- | |
chapter(s: "12. Solution D) ModelFor Environment") | |
/// Normally you would define a 'Model' | |
/// When making it Environment Aware we create a `ModelFor` | |
/// Then we add a typealias named `Model` | |
typealias Model = ModelFor<RealWorld> | |
typealias ModelInTenSeconds = ModelFor<WorldInTenSeconds> | |
typealias ModelYesterday = ModelFor<WorldYesterday> | |
typealias ModelYesterdayOffline = ModelFor<WorldYesterdayOffline> | |
/// ModelFor uses Environment to access all relevant dependencies | |
struct ModelFor<Env: Environment> { | |
let items: [Item] | |
var validItems: [Item] { | |
let now = Env.now | |
return items.filter { ($0.date > now ? $0.date : now) == $0.date } | |
} | |
func sync(then callback: @escaping ([Item]) -> ()) { | |
Env.webService.checkValid(items: validItems){ ok in | |
Env.loggingService.log(message: "Returned \(ok)") | |
callback(ok ? self.validItems : []) | |
} | |
} | |
} | |
/// a Test helper function to check an ModelFor | |
func check<E>(model: ModelFor<E>, hasValidItems validCount: Int, afterSync: Int? = nil){ | |
print("->", E.self, "has", model.validItems.count) | |
assert(model.validItems.count == validCount) | |
// check if after sync the count is correct as well | |
model.sync(then: { | |
let afterSyncCount = afterSync ?? validCount | |
print("after accessing server", afterSyncCount) | |
assert($0.count == afterSyncCount) | |
}) | |
} | |
section(s: "now") | |
let modelNow = Model(items: items) | |
check(model: modelNow, hasValidItems: 10) | |
section(s: "InTenMinutes") | |
let modelInTen = ModelInTenSeconds(items: items) | |
check(model: modelInTen, hasValidItems: 9) | |
section(s: "Yesterday") | |
let modelYesterday = ModelYesterday(items: items) | |
check(model: modelYesterday, hasValidItems: 10) | |
section(s: "Tomorrow") | |
let modelTomorrow = ModelFor<WorldTomorrow>(items: items) | |
check(model: modelTomorrow, hasValidItems: 0) | |
section(s: "YesterdayOffline") | |
let modelYesterdayOffline = ModelYesterdayOffline(items: items) | |
check(model: modelYesterdayOffline, hasValidItems: 10, afterSync: 0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
swift 3 update