Skip to content

Instantly share code, notes, and snippets.

@elm4ward
Last active February 3, 2018 23:05
Show Gist options
  • Save elm4ward/4d477f57550c571f00f203ed89c5d8c4 to your computer and use it in GitHub Desktop.
Save elm4ward/4d477f57550c571f00f203ed89c5d8c4 to your computer and use it in GitHub Desktop.
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)
@elm4ward
Copy link
Author

swift 3 update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment