UX Testing with Rx
Sample iOS code that accompanies this blog post: https://brent.is/writing/nysc-tests
Sample iOS code that accompanies this blog post: https://brent.is/writing/nysc-tests
@testable import App | |
import KIF | |
import SwiftDate | |
class ExampleTest: KIFTestCase { | |
func testClassReservation() { | |
// ...login, etc. | |
let tooLateToReserve = klass.reservationsEndAt + 15.minutes | |
timeTravel(to: tooLateToReserve) | |
// ...verify time-sensitive view state using KIF | |
} | |
} | |
protocol TimeTravel {} | |
extension XCTestCase: TimeTravel { | |
func timeTravel(to time: NSDate) { | |
NotificationCenter.post(.TimeTravel, ["to": time]) | |
} | |
} |
import RxSwift | |
class NotificationCenter { | |
enum NotificationType: String { | |
case TimeTravel | |
} | |
typealias NotificationInfo = [NSObject: AnyObject]? | |
class func post(type: NotificationType, _ info: NotificationInfo = [:]) { | |
NSNotificationCenter.defaultCenter() | |
.postNotificationName(type.rawValue, object: nil, userInfo: info) | |
} | |
class func observe(type: NotificationType) -> Observable<NSNotification> { | |
return | |
NSNotificationCenter.defaultCenter() | |
.rx_notification(type.rawValue) | |
} | |
} | |
class NotificationClosureWrapper<T>: AnyObject { | |
let closure: T | |
init(closure: T) { | |
self.closure = closure | |
} | |
} |
import RxSwift | |
import SwiftDate | |
struct Time { | |
static var time$: Observable<NSDate> { | |
let mutableTime$: Observable<NSDate> = | |
NotificationCenter.observe(.TimeTravel) | |
.map({ note -> NSDate? in | |
let to: NSDate? = note.userInfo?["to"] as? NSDate | |
return to | |
}) | |
.filterNil() | |
let systemTime$ = | |
Observable<Int>.interval(1.minute, scheduler: MainScheduler.instance) | |
.map { _ in NSDate() } | |
.startWith(NSDate()) | |
let overridableTime$ = | |
systemTime$ | |
.takeUntil(mutableTime$) | |
let time$ = | |
[mutableTime$, overridableTime$] | |
.toObservable() | |
.merge() | |
.shareReplayLatestWhileConnected() | |
return time$ | |
} | |
} | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
func application( | |
application: UIApplication, | |
didFinishLaunchingWithOptions | |
launchOptions: [NSObject: AnyObject]?) -> Bool { | |
let time$ = Time.time$ | |
let vc = ExampleKlassViewController(time$: time$) | |
self.window = UIWindow(frame: UIScreen.mainScreen().bounds) | |
self.window?.rootViewController = vc | |
self.window?.makeKeyAndVisible() | |
return true | |
} | |
} |
class KlassDetailViewController: UIViewController { | |
let klass: Klass | |
let time$: Observable<NSDate> | |
init(time$: Observable<NSDate>) { | |
self.time$ = time$ | |
super.init(nibName: nil, bundle: nil) | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
guard let view = self.view as? KlassDetailView else { return } | |
let klass = // ... | |
let klassDeadline$ = klass.reservationsEndAt$ | |
Observable | |
.combineLatest(klassDeadline$, time$) { ($0, $1) } | |
.subscribeNext { [unowned self] (deadline, time) in | |
view.render(deadline, time) | |
} | |
.addDisposableTo(rx_disposeBag) | |
} | |
} | |