diff --git a/RxExample/RxExample-iOSTests/Info.plist b/RxExample/RxExample-iOSTests/Info.plist
new file mode 100644
index 00000000..ba72822e
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/Info.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ BNDL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+
+
diff --git a/RxExample/RxExample-iOSTests/Mocks/MockGitHubAPI.swift b/RxExample/RxExample-iOSTests/Mocks/MockGitHubAPI.swift
new file mode 100644
index 00000000..de6d18a2
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/Mocks/MockGitHubAPI.swift
@@ -0,0 +1,31 @@
+//
+// MockGitHubAPI.swift
+// RxExample
+//
+// Created by Krunoslav Zaher on 12/29/15.
+// Copyright © 2015 Krunoslav Zaher. All rights reserved.
+//
+
+import Foundation
+import RxSwift
+
+class MockGitHubAPI : GitHubAPI {
+ let _usernameAvailable: String -> Observable
+ let _signup: (String, String) -> Observable
+
+ init(
+ usernameAvailable: (String) -> Observable = notImplemented(),
+ signup: (String, String) -> Observable = notImplemented()
+ ) {
+ _usernameAvailable = usernameAvailable
+ _signup = signup
+ }
+
+ func usernameAvailable(username: String) -> Observable {
+ return _usernameAvailable(username)
+ }
+
+ func signup(username: String, password: String) -> Observable {
+ return _signup(username, password)
+ }
+}
diff --git a/RxExample/RxExample-iOSTests/Mocks/MockWireframe.swift b/RxExample/RxExample-iOSTests/Mocks/MockWireframe.swift
new file mode 100644
index 00000000..7a1b4081
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/Mocks/MockWireframe.swift
@@ -0,0 +1,29 @@
+//
+// MockWireframe.swift
+// RxExample
+//
+// Created by Krunoslav Zaher on 12/29/15.
+// Copyright © 2015 Krunoslav Zaher. All rights reserved.
+//
+
+import Foundation
+import RxSwift
+
+class MockWireframe : Wireframe {
+ let _openURL: (NSURL) -> ()
+ let _promptFor: (String, Any, [Any]) -> Observable
+
+ init(openURL: (NSURL) -> () = notImplementedSync(),
+ promptFor: (String, Any, [Any]) -> Observable = notImplemented()) {
+ _openURL = openURL
+ _promptFor = promptFor
+ }
+
+ func openURL(URL: NSURL) {
+ _openURL(URL)
+ }
+
+ func promptFor(message: String, cancelAction: Action, actions: [Action]) -> Observable {
+ return _promptFor(message, cancelAction, actions.map { $0 as Any }).map { $0 as! Action }
+ }
+}
diff --git a/RxExample/RxExample-iOSTests/Mocks/NotImplementedStubs.swift b/RxExample/RxExample-iOSTests/Mocks/NotImplementedStubs.swift
new file mode 100644
index 00000000..97463805
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/Mocks/NotImplementedStubs.swift
@@ -0,0 +1,29 @@
+//
+// Mocks.swift
+// RxExample
+//
+// Created by Krunoslav Zaher on 12/29/15.
+// Copyright © 2015 Krunoslav Zaher. All rights reserved.
+//
+
+import Foundation
+import RxSwift
+import RxTests
+
+// MARK: Generic support code
+
+
+// MARK: Not implemented stubs
+
+func notImplemented() -> (T1) -> Observable {
+ return { _ in
+ fatalError()
+ return Observable.empty()
+ }
+}
+
+func notImplementedSync() -> (T1) -> Void {
+ return { _ in
+ fatalError()
+ }
+}
diff --git a/RxExample/RxExample-iOSTests/Mocks/ValidationResult+Equatable.swift b/RxExample/RxExample-iOSTests/Mocks/ValidationResult+Equatable.swift
new file mode 100644
index 00000000..904fd199
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/Mocks/ValidationResult+Equatable.swift
@@ -0,0 +1,30 @@
+//
+// ValidationResult+Equatable.swift
+// RxExample
+//
+// Created by Krunoslav Zaher on 12/29/15.
+// Copyright © 2015 Krunoslav Zaher. All rights reserved.
+//
+
+import Foundation
+
+// MARK: Equatable
+
+extension ValidationResult : Equatable {
+
+}
+
+func == (lhs: ValidationResult, rhs: ValidationResult) -> Bool {
+ switch (lhs, rhs) {
+ case (.OK, .OK):
+ return true
+ case (.Empty, .Empty):
+ return true
+ case (.Validating, .Validating):
+ return true
+ case (.Failed, .Failed):
+ return true
+ default:
+ return false
+ }
+}
\ No newline at end of file
diff --git a/RxExample/RxExample-iOSTests/RxExample_iOSTests.swift b/RxExample/RxExample-iOSTests/RxExample_iOSTests.swift
new file mode 100644
index 00000000..b8a302ec
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/RxExample_iOSTests.swift
@@ -0,0 +1,200 @@
+//
+// RxExample_iOSTests.swift
+// RxExample-iOSTests
+//
+// Created by Krunoslav Zaher on 12/28/15.
+// Copyright © 2015 Krunoslav Zaher. All rights reserved.
+//
+
+import XCTest
+
+import RxSwift
+import RxTests
+@testable import RxCocoa
+
+let resolution: NSTimeInterval = 0.2 // seconds
+
+// MARK: Concrete tests
+
+/**
+This is just an example of one way how this can be done.
+*/
+class RxExample_iOSTests
+ : XCTestCase {
+
+ let booleans = ["t" : true, "f" : false]
+ let events = ["x" : ()]
+ let errors = [
+ "#1" : NSError(domain: "Some unknown error maybe", code: -1, userInfo: nil),
+ "#u" : NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut, userInfo: nil)
+ ]
+ let validations = [
+ "e" : ValidationResult.Empty,
+ "f" : ValidationResult.Failed(message: ""),
+ "o" : ValidationResult.OK(message: "Validated"),
+ "v" : ValidationResult.Validating
+ ]
+
+ let stringValues = [
+ "u1" : "verysecret",
+ "u2" : "secretuser",
+ "u3" : "secretusername",
+ "p1" : "huge secret",
+ "p2" : "secret",
+ "e" : ""
+ ]
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // This is test of enabled user interface elements.
+ // I guess you could do this for view models, but this is probably overkill to
+ // do.
+ //
+ // It's probably more suitable for some vital components of your system, but
+ // the pricinciple is the same.
+ ////////////////////////////////////////////////////////////////////////////////
+ func testGitHubSignup_vanillaObservables_1_testEnabledUserInterfaceElements() {
+ let scheduler = TestScheduler(initialClock: 0, resolution: resolution, simulateProcessingDelay: false)
+
+ // mock the universe
+ let mockAPI = mockGithubAPI(scheduler)
+
+ // expected events and test data
+ let (
+ usernameEvents,
+ passwordEvents,
+ repeatedPasswordEvents,
+ loginTapEvents,
+
+ expectedValidatedUsernameEvents,
+ expectedSignupEnabledEvents
+ ) = (
+ scheduler.parseEventsAndTimes("e---u1----u2-----u3-----------------", values: stringValues).first!,
+ scheduler.parseEventsAndTimes("e----------------------p1-----------", values: stringValues).first!,
+ scheduler.parseEventsAndTimes("e---------------------------p2---p1-", values: stringValues).first!,
+ scheduler.parseEventsAndTimes("------------------------------------", values: events).first!,
+
+ scheduler.parseEventsAndTimes("e---v--f--v--f---v--o----------------", values: validations).first!,
+ scheduler.parseEventsAndTimes("f--------------------------------t---", values: booleans).first!
+ )
+
+ let wireframe = MockWireframe()
+ let validationService = GitHubDefaultValidationService(API: mockAPI)
+
+ let viewModel = GithubSignupViewModel1(
+ input: (
+ username: scheduler.createHotObservable(usernameEvents).asObservable(),
+ password: scheduler.createHotObservable(passwordEvents).asObservable(),
+ repeatedPassword: scheduler.createHotObservable(repeatedPasswordEvents).asObservable(),
+ loginTaps: scheduler.createHotObservable(loginTapEvents).asObservable()
+ ),
+ dependency: (
+ API: mockAPI,
+ validationService: validationService,
+ wireframe: wireframe
+ )
+ )
+
+ // run experiment
+ let recordedSignupEnabled = scheduler.record(viewModel.signupEnabled)
+ let recordedValidatedUsername = scheduler.record(viewModel.validatedUsername)
+
+ scheduler.start()
+
+ // validate
+ XCTAssertEqual(recordedValidatedUsername.events, expectedValidatedUsernameEvents)
+ XCTAssertEqual(recordedSignupEnabled.events, expectedSignupEnabledEvents)
+ }
+
+ func testGitHubSignup_drivers_2_testEnabledUserInterfaceElements() {
+ let scheduler = TestScheduler(initialClock: 0, resolution: resolution, simulateProcessingDelay: false)
+
+ // mock the universe
+ let mockAPI = mockGithubAPI(scheduler)
+
+ // expected events and test data
+ let (
+ usernameEvents,
+ passwordEvents,
+ repeatedPasswordEvents,
+ loginTapEvents,
+
+ expectedValidatedUsernameEvents,
+ expectedSignupEnabledEvents
+ ) = (
+ scheduler.parseEventsAndTimes("e---u1----u2-----u3-----------------", values: stringValues).first!,
+ scheduler.parseEventsAndTimes("e----------------------p1-----------", values: stringValues).first!,
+ scheduler.parseEventsAndTimes("e---------------------------p2---p1-", values: stringValues).first!,
+ scheduler.parseEventsAndTimes("------------------------------------", values: events).first!,
+
+ scheduler.parseEventsAndTimes("e---v--f--v--f---v--o----------------", values: validations).first!,
+ scheduler.parseEventsAndTimes("f--------------------------------t---", values: booleans).first!
+ )
+
+ let wireframe = MockWireframe()
+ let validationService = GitHubDefaultValidationService(API: mockAPI)
+
+ let viewModel = GithubSignupViewModel2(
+ input: (
+ username: scheduler.createHotObservable(usernameEvents).asDriver(onErrorJustReturn: ""),
+ password: scheduler.createHotObservable(passwordEvents).asDriver(onErrorJustReturn: ""),
+ repeatedPassword: scheduler.createHotObservable(repeatedPasswordEvents).asDriver(onErrorJustReturn: ""),
+ loginTaps: scheduler.createHotObservable(loginTapEvents).asDriver(onErrorJustReturn: ())
+ ),
+ dependency: (
+ API: mockAPI,
+ validationService: validationService,
+ wireframe: wireframe
+ )
+ )
+
+ /**
+ This is important because driver will try to ensure that elements are being pumped on main scheduler,
+ and that sometimes means that it will get queued using `dispatch_async` to main dispatch queue and
+ not get flushed until end of the test.
+
+ This method enables using mock schedulers for while testing drivers.
+ */
+ driveOnScheduler(scheduler) {
+ // run experiment
+ let recordedSignupEnabled = scheduler.record(viewModel.signupEnabled)
+ let recordedValidatedUsername = scheduler.record(viewModel.validatedUsername)
+
+ scheduler.start()
+
+ // validate
+ XCTAssertEqual(recordedValidatedUsername.events, expectedValidatedUsernameEvents)
+ XCTAssertEqual(recordedSignupEnabled.events, expectedSignupEnabledEvents)
+ }
+ }
+}
+
+// MARK: Mocks
+
+extension RxExample_iOSTests {
+ func mockGithubAPI(scheduler: TestScheduler) -> GitHubAPI {
+ return MockGitHubAPI(
+ usernameAvailable: scheduler.mock(booleans, errors: errors) { (username) -> String in
+ if username == "secretusername" {
+ return "---t"
+ }
+ else if username == "secretuser" {
+ return "---#1"
+ }
+ else {
+ return "---f"
+ }
+ },
+ signup: scheduler.mock(booleans, errors: errors) { (username, password) -> String in
+ if username == "secretusername" && password == "secret" {
+ return "--t"
+ }
+ else {
+ return "--f"
+ }
+ }
+ )
+ }
+}
+
+// MARK: Mocks
+
diff --git a/RxExample/RxExample-iOSTests/TestScheduler+MarbleTests.swift b/RxExample/RxExample-iOSTests/TestScheduler+MarbleTests.swift
new file mode 100644
index 00000000..a758e3e4
--- /dev/null
+++ b/RxExample/RxExample-iOSTests/TestScheduler+MarbleTests.swift
@@ -0,0 +1,198 @@
+//
+// TestScheduler+MarbleTests.swift
+// RxExample
+//
+// Created by Krunoslav Zaher on 12/29/15.
+// Copyright © 2015 Krunoslav Zaher. All rights reserved.
+//
+
+import Foundation
+import RxSwift
+import RxTests
+import RxCocoa
+
+/**
+There are examples like this all over the web, but I think that I've first something like this here
+ https://github.com/ReactiveX/RxJS/blob/master/doc/writing-marble-tests.md
+
+These tests are called marble tests.
+
+*/
+extension TestScheduler {
+ /**
+ Transformation from this format:
+
+ ---a---b------c-----
+
+ to this format
+
+ schedule onNext(1) @ 0.6s
+ schedule onNext(2) @ 1.4s
+ schedule onNext(3) @ 7.0s
+ ....
+ ]
+
+ You can also specify retry data in this format:
+
+ ---a---b------c----#|----a--#|----b
+
+ - letters and digits mark values
+ - `#` marks unknown error
+ - `|` marks sequence completed
+
+ */
+ func parseEventsAndTimes(timeline: String, values: [String: T], errors: [String: ErrorType] = [:]) -> [[Recorded>]] {
+ print("parsing: \(timeline)")
+ typealias RecordedEvent = Recorded>
+ let timelines = timeline.componentsSeparatedByString("|")
+
+ let allExceptLast = timelines[0 ..< timelines.count - 1]
+
+ return (allExceptLast.map { $0 + "|" } + [timelines.last!])
+ .filter { $0.characters.count > 0 }
+ .map { timeline -> [Recorded>] in
+ let segments = timeline.componentsSeparatedByString("-")
+ let (time: _, events: events) = segments.reduce((time: 0, events: [RecordedEvent]())) { state, event in
+ let tickIncrement = event.characters.count + 1
+
+ if event.characters.count == 0 {
+ return (state.time + tickIncrement, state.events)
+ }
+
+ if event == "#" {
+ let errorEvent = RecordedEvent(time: state.time, event: Event.Error(NSError(domain: "Any error domain", code: -1, userInfo: nil)))
+ return (state.time + tickIncrement, state.events + [errorEvent])
+ }
+
+ if event == "|" {
+ let completed = RecordedEvent(time: state.time, event: Event.Completed)
+ return (state.time + tickIncrement, state.events + [completed])
+ }
+
+ guard let next = values[event] else {
+ guard let error = errors[event] else {
+ fatalError("Value with key \(event) not registered as value:\n\(values)\nor error:\n\(errors)")
+ }
+
+ let nextEvent = RecordedEvent(time: state.time, event: Event.Error(error))
+ return (state.time + tickIncrement, state.events + [nextEvent])
+ }
+
+ let nextEvent = RecordedEvent(time: state.time, event: Event.Next(next))
+ return (state.time + tickIncrement, state.events + [nextEvent])
+ }
+
+ print("parsed: \(events)")
+ return events
+ }
+ }
+
+ /**
+ Creates driver for marble test.
+
+ - parameter timeline: Timeline in the form `---a---b------c--|`
+ - parameter values: Dictionary of values in timeline. `[a:1, b:2]`
+
+ - returns: Driver specified by timeline and values.
+ */
+ func createDriver(timeline: String, values: [String: T]) -> Driver {
+ return createObservable(timeline, values: values, errors: [:]).asDriver(onErrorRecover: { (error) -> Driver in
+ fatalError("This can't error out")
+ return Driver.never()
+ })
+ }
+
+ /**
+ Creates observable for marble tests.
+
+ - parameter timeline: Timeline in the form `---a---b------c--|`
+ - parameter values: Dictionary of values in timeline. `[a:1, b:2]`
+ - parameter errors: Dictionary of errors in timeline.
+
+ - returns: Observable sequence specified by timeline and values.
+ */
+ func createObservable(timeline: String, values: [String: T], errors: [String: ErrorType] = [:]) -> Observable {
+ let events = self.parseEventsAndTimes(timeline, values: values, errors: errors)
+ return createObservable(events)
+ }
+
+ /**
+ Creates observable for marble tests.
+
+ - parameter events: Recorded events to replay.
+
+ - returns: Observable sequence specified by timeline and values.
+ */
+ func createObservable(events: [Recorded>]) -> Observable {
+ return createObservable([events])
+ }
+
+ /**
+ Creates observable for marble tests.
+
+ - parameter events: Recorded events to replay. This overloads enables modeling of retries.
+ `---a---b------c----#|----a--#|----b`
+ When next observer is subscribed, next sequence will be replayed. If all sequences have
+ been replayed and new observer is subscribed, `fatalError` will be raised.
+
+ - returns: Observable sequence specified by timeline and values.
+ */
+ func createObservable(events: [[Recorded>]]) -> Observable {
+ var attemptCount = 0
+ print("created for \(events)")
+
+ return Observable.create { observer in
+ if attemptCount >= events.count {
+ fatalError("This is attempt # \(attemptCount + 1), but timeline only allows \(events.count).\n\(events)")
+ }
+
+ let scheduledEvents = events[attemptCount].map { event in
+ return self.scheduleRelative((), dueTime: resolution * NSTimeInterval(event.time)) { _ in
+ observer.on(event.value)
+ return NopDisposable.instance
+ }
+ }
+
+ attemptCount += 1
+
+ return CompositeDisposable(disposables: scheduledEvents)
+ }
+ }
+
+ /**
+ Enables simple construction of mock implementations from marble timelines.
+
+ - parameter Arg: Type of arguments of mocked method.
+ - parameter Ret: Return type of mocked method. `Observable`
+
+ - parameter values: Dictionary of values in timeline. `[a:1, b:2]`
+ - parameter errors: Dictionary of errors in timeline.
+ - parameter timelineSelector: Method implementation. The returned string value represents timeline of
+ returned observable sequence. `---a---b------c----#|----a--#|----b`
+
+ - returns: Implementation of method that accepts arguments with parameter `Arg` and returns observable sequence
+ with parameter `Ret`.
+ */
+ func mock(values: [String: Ret], errors: [String: ErrorType] = [:], timelineSelector: Arg -> String) -> Arg -> Observable {
+ return { (parameters: Arg) -> Observable in
+ let timeline = timelineSelector(parameters)
+
+ return self.createObservable(timeline, values: values, errors: errors)
+ }
+ }
+
+ /**
+ Builds testable observer for s specific observable sequence, binds it's results and sets up disposal.
+
+ - parameter source: Observable sequence to observe.
+ - returns: Observer that records all events for observable sequence.
+ */
+ func record(source: O) -> TestableObserver {
+ let observer = self.createObserver(O.E.self)
+ let disposable = source.asObservable().bindTo(observer)
+ self.scheduleAt(100000) {
+ disposable.dispose()
+ }
+ return observer
+ }
+}