Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Venturing into the world of RxSwift unit testing

Venturing into the world of RxSwift unit testing

Reactive programming is an emerging discipline that allows to write declarative, asynchronous and concurrent code in a functional way and is continuously gaining popularity and adoption.

In this talk we will wander in the unexplored pathways of RxSwift testing infrastructure. Specifically, we will look into the key aspects of testing RxSwift code and we will analyze the different ways to unit test observable streams through a simple sign in app.

Presented in MobileOptimized in Minsk in 2019:
https://www.youtube.com/watch?v=49K6MMhn-fc

Presented in NSSpain in Logrono, Spain in 2019:
https://vimeo.com/362134004?fbclid=IwAR04dN8rRMFIVkR-7bEUY3R_9uEkq8WN_A13Kj1E_C9Fa8TFpcXqPjCXVDI

Presented in 360iDev in Denver, Co, USA in 2019:
https://vimeo.com/365321297

Eleni Papanikolopoulou

October 19, 2019
Tweet

More Decks by Eleni Papanikolopoulou

Other Decks in Programming

Transcript

  1. enum Event<Element> { case next(Element) // next element of a

    sequence case error(Swift.Error) // sequence failed with error case completed // sequence terminated successfully } 2. Events error next completed
  2. 3. Observers Closure that is passed to the Observable in

    order to receive the sequence elements Listen for the events subsribe
  3. 5. Operators Transform, combine or filter the events emitted map

    flatMap combineLatest filter merge startWith withLatestFrom skip take skipUntil takeUntil zip just catchError distinctUntilChanged sample takeLast scan concat startWith switchLatest skipWhile takeWhile takeWhileWithIndex empty never throw retry debounce ignoreElements sample throttle of single reduce flatMapFirst flatMapLatest RxMarbles ✅
  4. 7. Schedulers ⏰ ✅ to force operators do their work

    on a specific queue MainScheduler SerialDispatchQueueScheduler CurrentThreadScheduler ConcurrentDispatchQueueScheduler OperationQueueScheduler
  5. View Model Observable< > Observable< > Observable< > Observable< >

    Observable< > Observable< > Observable< > Inputs Outputs
  6. The View Model class SignInViewModel { //input var emailText: Observable<String>!

    var passwordText: Observable<String>! var signInButtonTap: Observable<Void>!
  7. The View Model class SignInViewModel { //input var emailText: Observable<String>!

    var passwordText: Observable<String>! var signInButtonTap: Observable<Void>! //output var emailIsValid: Observable<Bool>! var passwordIsValid: Observable<Bool>! var signInButtonEnabled: Observable<Bool>! var signIn: Observable<SignInResponse>!
  8. The View Model class SignInViewModel { //input var emailText: Observable<String>!

    var passwordText: Observable<String>! var signInButtonTap: Observable<Void>! //output var emailIsValid: Observable<Bool>! var passwordIsValid: Observable<Bool>! var signInButtonEnabled: Observable<Bool>! var signIn: Observable<SignInResponse>! init(signInAPI: SignInAPIType, disposeBag: DisposeBag) { self.signInAPI = signInAPI self.disposeBag = disposeBag } }
  9. / // Email let emailRegexMatcher = RegexMatcher(regex: ““email_regex””) emailIsValid =

    emailText.map({ return emailRegexMatcher.matches(string: $0) }) Observable< String > Observable< Bool > map } Configuring the view model //input func configure(emailText: Observable<String>, passwordText: Observable<String>, signInButtonTap: Observable<Void>) { }
  10. Configuring the view model //input func configure(emailText: Observable<String>, passwordText: Observable<String>,

    signInButtonTap: Observable<Void>) { // Email let emailRegexMatcher = RegexMatcher(regex: "email_regex") emailIsValid = emailText.map({ return emailRegexMatcher.matches(string: $0)}) // Password let passwordRegexMatcher = RegexMatcher(regex: “password_regex”) passwordIsValid = passwordText.map({ return passwordRegexMatcher.matches(string:$0)}) }
  11. Configuring the view model //input func configure(emailText: Observable<String>, passwordText: Observable<String>,

    signInButtonTap: Observable<Void>) { // Email let emailRegexMatcher = RegexMatcher(regex: "email_regex") emailIsValid = emailText.map({ return emailRegexMatcher.matches(string: $0)}) // Password let passwordRegexMatcher = RegexMatcher(regex: “password_regex”) passwordIsValid = passwordText.map({ return passwordRegexMatcher.matches(string:$0)}) } // SignIn Button signInButtonEnabled = Observable.combineLatest(emailIsValid, passwordIsValid { (emailIsValid, passwordIsValid) -> Bool in return emailIsValid && passwordIsValid }.startWith(false)
  12. Configuring the view model //input func configure(emailText: Observable<String>, passwordText: Observable<String>,

    signInButtonTap: Observable<Void>) { // Email let emailRegexMatcher = RegexMatcher(regex: "email_regex") emailIsValid = emailText.map({ return emailRegexMatcher.matches(string: $0)}) // Password let passwordRegexMatcher = RegexMatcher(regex: “password_regex”) passwordIsValid = passwordText.map({ return passwordRegexMatcher.matches(string:$0)}) } // Sign In Action signIn = Observable.combineLatest(emailText, emailIsValid, passwordText, passwordIsValid, signInButtonTap, resultSelector: { (emailText, emailIsValid, passwordText, passwordIsValid, signInTap) -> SignInDetails? in guard emailIsValid, passwordIsValid else { return nil } return SignInDetails(emailText: emailText, passwordText: passwordText) }) .unwrap() .flatMapLatest({[weak self](signUpDetails) -> Observable<SignInResponse> in guard let strongSelf = self else { return .empty() } return strongSelf.signInAPI.signIn(with: signUpDetails) }) } // SignIn Button signInButtonEnabled = Observable.combineLatest(emailIsValid, passwordIsValid { (emailIsValid, passwordIsValid) -> Bool in return emailIsValid && passwordIsValid }.startWith(false)
  13. // Sign In Action signIn = Observable.combineLatest(emailText, emailIsValid, passwordText, passwordIsValid,

    signInButtonTap, resultSelector: { (emailText, emailIsValid, passwordText, passwordIsValid, signInTap) -> SignInDetails? in guard emailIsValid, passwordIsValid else { return nil } return SignInDetails(emailText: emailText, passwordText: passwordText) }) .unwrap() .flatMapLatest({[weak self](signUpDetails) -> Observable<SignInResponse> in guard let strongSelf = self else { return .empty() } return strongSelf.signInAPI.signIn(with: signUpDetails) }) }
  14. .unwrap() -> import RxSwiftExt .combineLatest() .flatMapLatest() /* Takes a sequence

    of optional elements and returns a sequence of non-optional elements, filtering out any nil values. - returns: An observable sequence of non-optional elements */
  15. RxMagic… // Sign In Action signIn = Observable.combineLatest(emailText, emailIsValid, passwordText,

    passwordIsValid, signInButtonTap, resultSelector: { (emailText, emailIsValid, passwordText, passwordIsValid, signInTap) -> SignInDetails? in guard emailIsValid, passwordIsValid else { return nil } return SignInDetails(emailText: emailText, passwordText: passwordText) }) .unwrap() .flatMapLatest({[weak self](signUpDetails) -> Observable<SignInResponse> in guard let strongSelf = self else { return .empty() } return strongSelf.signInAPI.signIn(with: signUpDetails) }) }
  16. protocol SignInAPIType { func signIn(with signInDetails: SignInDetails) -> Observable<SignInResponse> }

    struct SignInResponse: Decodable { let token: String } The Sign In API class SignInAPI: SignInAPIType { let httpClient: HttpClient = HttpClient.sharedService func signIn(with signInDetails: SignInDetails) -> Observable<SignInResponse> { let parameters: [String: Any] = ["email:" : signInDetails.emailText, "password": signInDetails.passwordText] return httpClient.loadRequestWithPath("/backend/api/signin", parameters: parameters, method: .POST, headers: []) } }
  17. The Sign In View Controller class SignInViewController: UIViewController { @IBOutlet

    weak var emailTextfield: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var signInButton: UIButton! @IBOutlet weak var emailErrorLabel: UILabel! @IBOutlet weak var passwordErrorLabel: UILabel! }
  18. The Sign In View Controller class SignInViewController: UIViewController { @IBOutlet

    weak var emailTextfield: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var signInButton: UIButton! @IBOutlet weak var emailErrorLabel: UILabel! @IBOutlet weak var passwordErrorLabel: UILabel! private let disposeBag = DisposeBag() private var viewModel: SignInViewModel! override func viewDidLoad() { super.viewDidLoad() viewModel = SignInViewModel(signInAPI: SignInAPI(), disposeBag: disposeBag) } }
  19. The Sign In View Controller class SignInViewController: UIViewController { @IBOutlet

    weak var emailTextfield: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var signInButton: UIButton! @IBOutlet weak var emailErrorLabel: UILabel! @IBOutlet weak var passwordErrorLabel: UILabel! private let disposeBag = DisposeBag() private var viewModel: SignInViewModel! override func viewDidLoad() { super.viewDidLoad() viewModel = SignInViewModel(signInAPI: SignInAPI(), disposeBag: disposeBag) } } let email = emailTextfield.rx.text.asObservable().filter({ $0.isEmpty }) viewModel.configure(emailText: email,
  20. The Sign In View Controller } let email = emailTextfield.rx.text.asObservable().filter({

    $0.isEmpty }) viewModel.configure(emailText: email, override func viewDidLoad() { super.viewDidLoad() viewModel = SignInViewModel(signInAPI: SignInAPI(), disposeBag: disposeBag) let password = passwordTextField.rx.text.asObservable().filter({ !$0.isEmpty }) let signInButtonTap = signInButton.rx.tap.asObservable() passwordText: password, signInButtonTap: signInButtonTap)
  21. //subsribe to the outputs viewModel.emailIsValid.bind(to: emailErrorLabel.rx.isHidden).disposed(by: disposeBag) override func viewDidLoad()

    { super.viewDidLoad() viewModel = SignInViewModel(signInAPI: SignInAPI(), disposeBag: disposeBag) let email = emailTextfield.rx.text.asObservable().filter({ !$0.isEmpty }) let password = passwordTextField.rx.text.asObservable().filter({ !$0.isEmpty }) let signInButtonTap = signInButton.rx.tap.asObservable() viewModel.configure(emailText: email, passwordText: password, signInButtonTap: signInButtonTap) }
  22. override func viewDidLoad() { super.viewDidLoad() viewModel = SignInViewModel(signInAPI: SignInAPI(), disposeBag:

    disposeBag) let email = emailTextfield.rx.text.asObservable().filter({ !$0.isEmpty }) let password = passwordTextField.rx.text.asObservable().filter({!$0.isEmpty }) let signInButtonTap = signInButton.rx.tap.asObservable() viewModel.configure(emailText: email, passwordText: password, signInButtonTap: signInButtonTap) //subsribe to the outputs viewModel.emailIsValid.bind(to: emailErrorLabel.rx.isHidden).disposed(by: disposeBag) viewModel.passwordIsValid.bind(to: passwordErrorLabel.rx.isHidden).disposed(by: disposeBag) viewModel.signInButtonEnabled.bind(to: signInButton.rx.isEnabled).disposed(by: disposeBag) }
  23. override func viewDidLoad() { super.viewDidLoad() viewModel = SignInViewModel(signInAPI: SignInAPI(), disposeBag:

    disposeBag) let email = emailTextfield.rx.text.asObservable().filter({ !$0.isEmpty }) let password = passwordTextField.rx.text.asObservable().filter({!$0.isEmpty }) let signInButtonTap = signInButton.rx.tap.asObservable() viewModel.configure(emailText: email, passwordText: password, signInButtonTap: signInButtonTap) //subsribe to the outputs viewModel.emailIsValid.bind(to: emailErrorLabel.rx.isHidden).disposed(by: disposeBag) viewModel.passwordIsValid.bind(to: passwordErrorLabel.rx.isHidden).disposed(by: disposeBag) viewModel.signInButtonEnabled.bind(to: signInButton.rx.isEnabled).disposed(by: disposeBag) viewModel.signIn .observeOn(MainScheduler.instance) .subscribe( onNext: { [weak self] (response) in self?.showAlert(for: response) }, onError: { [weak self] error in self?.showAlert(for: error) } ).disposed(by: disposeBag) }
  24. Quick/Nimble Quick is a behavior-driven development (BDD) framework to write

    tests as specs in simple structures. Nimble is a matcher framework that is expressive and supports asynchronous tests.
  25. Quick/Nimble Quick is a behavior-driven development (BDD) framework to write

    tests as specs in simple structures. Nimble is a matcher framework that is expressive and supports asynchronous tests. With Quick and Nimble, each test is written in as an it closure, and each expectation is expressed as:
  26. Quick/Nimble ☑ Expectation: expect(something).to(condition) or expect(something).toNot(condition) Quick is a behavior-driven

    development (BDD) framework to write tests as specs in simple structures. Nimble is a matcher framework that is expressive and supports asynchronous tests. With Quick and Nimble, each test is written in as an it closure, and each expectation is expressed as:
  27. Quick/Nimble override func spec() { describe("The Subject Under Test") {

    beforeEach { // initialization code } context("when a condition applies") { beforeEach { // condition initialization code } it("expectation description”) { // expectation - assertion } } } }
  28. class SignInAPIFake: SignInAPIType { var signInCalled = false var signInReturnValue:

    PublishSubject<SignInResponse>! func signIn(with signInDetails: SignInDetails) -> Observable<SignInResponse> { signInReturnValue = PublishSubject() return signInReturnValue.asObservable() .do(onSubscribe: {[weak self] in self?.signInCalled = true }) } } Mock the network layer
  29. Different ways Set the timeline of events ☑ by using

    RxBlocking ☑ by using PublishSubject
  30. Different ways Set the timeline of events ☑ by using

    RxBlocking ☑ by using TestScheduler ☑ by using PublishSubject
  31. Different ways Set the timeline of events ☑ by using

    RxBlocking ☑ by using Marble Syntax ☑ by using TestScheduler ☑ by using PublishSubject
  32. Different ways Set the timeline of events ☑ by using

    RxBlocking ☑ by using Marble Syntax ☑ by using TestScheduler ☑ by using PublishSubject
  33. class SignInViewModelPublishSubjectSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { var sut: SignInViewModel! var signInAPI: SignInAPIFake! var disposeBag: DisposeBag!
  34. class SignInViewModelPublishSubjectSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { var sut: SignInViewModel! var signInAPI: SignInAPIFake! var disposeBag: DisposeBag! // Fake the events var emailText: PublishSubject<String>! var passwordText: PublishSubject<String>! var signInButtonTap: PublishSubject<Void>!
  35. class SignInViewModelPublishSubjectSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { var sut: SignInViewModel! var signInAPI: SignInAPIFake! var disposeBag: DisposeBag! // Fake the events var emailText: PublishSubject<String>! var passwordText: PublishSubject<String>! var signInButtonTap: PublishSubject<Void>! // Results var emailIsValid: Bool! var passwordIsValid: Bool! var signInButtonEnabled: Bool! var signInResponse: SignInResponse! } } }
  36. beforeEach { signInAPI = SignInAPIFake() disposeBag = DisposeBag() sut =

    SignInViewModel(signInAPI: signInAPI, disposeBag: disposeBag)
  37. beforeEach { signInAPI = SignInAPIFake() disposeBag = DisposeBag() sut =

    SignInViewModel(signInAPI: signInAPI, disposeBag: disposeBag) emailText = PublishSubject<String>() passwordText = PublishSubject<String>() signInButtonTap = PublishSubject<Void>()
  38. beforeEach { signInAPI = SignInAPIFake() disposeBag = DisposeBag() sut =

    SignInViewModel(signInAPI: signInAPI, disposeBag: disposeBag) emailText = PublishSubject<String>() passwordText = PublishSubject<String>() signInButtonTap = PublishSubject<Void>() // Fake the inputs sut.configure(emailText: emailText, passwordText: passwordText, signInButtonTap: signInButtonTap) }
  39. sut.passwordIsValid.subscribe(onNext: { (isValid) in passwordIsValid = isValid }).disposed(by: disposeBag) sut.signInButtonEnabled.subscribe(onNext:

    { (enabled) in signInButtonEnabled = enabled }).disposed(by: disposeBag) sut.signIn.subscribe(onNext: { (response) in signInResponse = response }).disposed(by: disposeBag) beforeEach { // “Listen” the events sut.emailIsValid.subscribe(onNext: { (isValid) in emailIsValid = isValid }).disposed(by: disposeBag) }
  40. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabled).to(beFalse()) }
  41. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) } }
  42. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) } it("should get an event that email is invalid") { expect(emailIsValid).to(beFalse()) } }
  43. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) } it("should get an event that email is invalid") { expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) } } }
  44. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) } it("should get an event that email is invalid") { expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) } it("should get an event that password is invalid") { expect(passwordIsValid).to(beFalse()) } } }
  45. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) } it("should get an event that email is invalid") { expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) } it("should get an event that password is invalid") { expect(passwordIsValid).to(beFalse()) } it("should get another event that sign in button should not be enabled") { expect(signInButtonEnabled).to(beFalse()) } } }
  46. context("when valid email and password are typed") { beforeEach {

    emailText.on(.next(“[email protected]”)) passwordText.on(.next("Asd123!!")) } }
  47. context("when valid email and password are typed") { beforeEach {

    emailText.on(.next(“[email protected]”)) passwordText.on(.next("Asd123!!")) } it("should get an event that email is valid") { expect(emailIsValid).to(beTrue()) } it("should get an event that password is valid") { expect(passwordIsValid).to(beTrue()) } it("should get an event that sign in button should be enabled") { expect(signInButtonEnabled).to(beTrue()) } }
  48. context("when sign in button is tapped") { beforeEach { signInButtonTap.on(.next(()))

    } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } }
  49. context("when sign in button is tapped") { beforeEach { signInButtonTap.on(.next(()))

    } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } context("when sign in response arrives") { beforeEach { let signInResponse = SignInResponse(token: "fake_token") signInAPI.signInReturnValue.on(.next(signInResponse)) } } }
  50. context("when sign in button is tapped") { beforeEach { signInButtonTap.on(.next(()))

    } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } context("when sign in response arrives") { beforeEach { let signInResponse = SignInResponse(token: "fake_token") signInAPI.signInReturnValue.on(.next(signInResponse)) } it("should give an event with the correct sign in response") { expect(signInResponse.token).to(equal("fake_token")) } } }
  51. class SignInViewModelPublishSubjectSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { beforeEach { //initialization } it("should get an initial event that sign in should not be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) } it("should get an event that email is invalid") { expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) } it("should get an event that password is invalid") { expect(passwordIsValid).to(beFalse()) } it("should get another event that sign in button should not be enabled") { expect(signInButtonEnabled).to(beFalse()) } context("when valid email and password are typed") { beforeEach { emailText.on(.next("[email protected]")) passwordText.on(.next("Asd123!!")) } it("should get an event that email is valid") { expect(emailIsValid).to(beTrue()) } it("should get an event that password is valid") { expect(passwordIsValid).to(beTrue()) } it("should get an event that sign in button should be enabled") { expect(signInButtonEnabled).to(beTrue()) } context("when sign in button is tapped") { beforeEach { signInButtonTap.on(.next(())) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } context("when sign in response arrives") { beforeEach { let signInResponse = SignInResponse(token: "fake_token") signInAPI.signInReturnValue.on(.next(signInResponse)) } it("should give an event with the correct sign in response") { expect(signInResponse.token).to(equal("fake_token")) } } } } } } } } }
  52. Easy to write Don’t have to think timeline of events

    right from the start Don’t have to think expected events right from the start ❌ Nesting ❌ Do not test when events are emitted Easy to read
  53. Set the timeline of events ☑ by using PublishSubject ☑

    by using RxBlocking ☑ by using TestScheduler ☑ by using Marble Syntax
  54. RxBlocking ReplaySubject import RxBlocking Blocks the current thread and waits

    for the sequence to complete, allowing to synchronously access its result .toBlocking() => converts a regular observable sequence to a blocking observable .toArray() => returns an array of elements emitted once a sequence is completed PublishSubject doesn’t work with RxBlocking (doesn’t replay the events)
  55. var emailText: ReplaySubject<String>! var passwordText: ReplaySubject<String>! var signInButtonTap: ReplaySubject<Void>! beforeEach

    { //Use ReplaySubject instead of PublishSubject emailText = ReplaySubject<String>.create(bufferSize: 10) passwordText = ReplaySubject<String>.create(bufferSize: 10) signInButtonTap = ReplaySubject<Void>.create(bufferSize: 10) sut.configure(emailText: emailText, passwordText: passwordText, signInButtonTap: signInButtonTap) } } } class SignInViewModelRxBlockingSpec: QuickSpec { override func spec() { describe("The SignInViewModel") { }
  56. class SignInViewModelRxBlockingSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { // <initial properties here> var emailText: ReplaySubject<String>! var passwordText: ReplaySubject<String>! var signInButtonTap: ReplaySubject<Void>! beforeEach { //Use ReplaySubject instead of PublishSubject emailText = ReplaySubject<String>.create(bufferSize: 10) passwordText = ReplaySubject<String>.create(bufferSize: 10) signInButtonTap = ReplaySubject<Void>.create(bufferSize: 10) sut.configure(emailText: emailText, passwordText: passwordText, signInButtonTap: signInButtonTap) } } }
  57. it("should get an initial event that sign in should not

    be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().first() expect(signInButtonEnabled).to(beFalse()) } it("should get another event that sign in button should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().toArray() expect(signInButtonEnabled).to(equal([false, false])) }
  58. context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@"))

    emailText.on(.completed) } it("should get an event that email is invalid") { let emailIsValid = try! sut.emailIsValid.toBlocking().first() expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) passwordText.on(.completed) } it("should get an event that password is invalid") { let passwordIsValid = try! sut.passwordIsValid.toBlocking().first() expect(passwordIsValid).to(beFalse()) } it("should get another event that sign in button should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().toArray() expect(signInButtonEnabled).to(equal([false, false])) } } } it("should get an initial event that sign in should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().first() expect(signInButtonEnabled).to(beFalse()) }
  59. context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@"))

    emailText.on(.completed) } it("should get an event that email is invalid") { let emailIsValid = try! sut.emailIsValid.toBlocking().first() expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) passwordText.on(.completed) } it("should get an event that password is invalid") { let passwordIsValid = try! sut.passwordIsValid.toBlocking().first() expect(passwordIsValid).to(beFalse()) } it("should get another event that sign in button should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().toArray() expect(signInButtonEnabled).to(equal([false, false])) } } } it("should get an initial event that sign in should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().first() expect(signInButtonEnabled).to(beFalse()) }
  60. context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@"))

    emailText.on(.completed) } it("should get an event that email is invalid") { let emailIsValid = try! sut.emailIsValid.toBlocking().first() expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) passwordText.on(.completed) } it("should get an event that password is invalid") { let passwordIsValid = try! sut.passwordIsValid.toBlocking().first() expect(passwordIsValid).to(beFalse()) } it("should get another event that sign in button should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().toArray() expect(signInButtonEnabled).to(equal([false, false])) } } } it("should get an initial event that sign in should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking().first() expect(signInButtonEnabled).to(beFalse()) }
  61. class SignInViewModelRxBlockingSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { beforeEach { //initialization code here } it("should get an initial event that sign in should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking(timeout: 3).first() expect(signInButtonEnabled).to(beFalse()) } context("when an invalid email is typed") { beforeEach { emailText.on(.next("test@")) emailText.on(.completed) } it("should get an event that email is invalid") { let emailIsValid = try! sut.emailIsValid.toBlocking(timeout: 3).toArray().first expect(emailIsValid).to(beFalse()) } context("when an invalid password is typed") { beforeEach { passwordText.on(.next("asd")) passwordText.on(.completed) } it("should get an event that password is invalid") { let passwordIsValid = try! sut.passwordIsValid.toBlocking(timeout: 3).first() expect(passwordIsValid).to(beFalse()) } it("should get another event that sign in button should not be enabled") { let signInButtonEnabled = try! sut.signInButtonEnabled.toBlocking(timeout: 3).toArray() expect(signInButtonEnabled).to(equal([false, false])) } } } context("when valid email and password are typed") { beforeEach { emailText.on(.next("[email protected]")) emailText.on(.completed) passwordText.on(.next("Asd123!!")) passwordText.on(.completed) } it("should get an event that email is valid") { let emailIsValidSecondEvent = try! sut.emailIsValid.toBlocking(timeout: 3).first() expect(emailIsValidSecondEvent).to(beTrue()) } it("should get an event that password is valid") { let passwordIsValidSecondEvent = try! sut.passwordIsValid.toBlocking(timeout: 3).first() expect(passwordIsValidSecondEvent).to(beTrue()) } it("should get an event that sign in button should be enabled") { let signInButtonEnabledThirdEvent = try! sut.signInButtonEnabled.toBlocking(timeout: 3).toArray() expect(signInButtonEnabledThirdEvent).to(equal([false, true])) } context("when sign in button is tapped") { beforeEach { rememberEmail.on(.next(false)) rememberEmail.on(.completed) signInButtonTap.on(.next(())) signInButtonTap.on(.completed) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } context("when response arrives") { beforeEach { signInAPI.signInReturnValue.on(.next(SignInResponse(email: "[email protected]", account: "workable"))) signInAPI.signInReturnValue.onCompleted() } it("should have the correct response emition") { let signInAction = try? sut.signInAction.toBlocking(timeout: 3).first() expect(signInAction).to(equal(SignInResponse(email: "[email protected]", account: "workable"))) } } } } } } }
  62. ❌ Additional completed events Easy to write We don’t have

    to think expected events right from the start Easy to read We don’t have to think timeline of events right from the start ❌ Can’t use a PublishSubject ❌ Do not test when events are emitted ❌ Can’t use for non-terminating sequences ❌ Blocks current thread thus tests taking longer
  63. Set the timeline of events ☑ by using PublishSubject ☑

    by using RxBlocking ☑ by using TestScheduler ☑ by using Marble Syntax
  64. TestScheduler •Provided by RxTest => import RxTest •It simplifies testing

    time-based events •Enables creating mock Observables and Observers •Enables us to “record” these events and test them •Clock: Virtual Time
  65. TestScheduler • Provided by RxTest => import RxTest •It simplifies

    testing time-based events •Enables creating mock Observables and Observers •Enables us to “record” these events and test them •Clock: Virtual Time
  66. TestScheduler • Provided by RxTest => import RxTest • It

    simplifies testing time-based events •Enables creating mock Observables and Observers •Enables us to “record” these events and test them •Clock: Virtual Time
  67. TestScheduler • Provided by RxTest => import RxTest • It

    simplifies testing time-based events • Enables creating mock Observables and Observers •Enables us to “record” these events and test them •Clock: Virtual Time
  68. TestScheduler • Provided by RxTest => import RxTest • It

    simplifies testing time-based events • Enables creating mock Observables and Observers • Enables us to “record” these events and test them •Clock: Virtual Time
  69. TestScheduler • Provided by RxTest => import RxTest • It

    simplifies testing time-based events • Enables creating mock Observables and Observers • Enables us to “record” these events and test them • Clock: Virtual Time
  70. Testable Observer // Listen for the events sut.emailIsValid.subscribe(emailIsValidObserver).disposed(by: disposeBag) let

    scheduler = TestScheduler(initialClock: 0) var emailIsValidObserver: TestableObserver<Bool> = scheduler.createObserver(Bool.self)
  71. Testable Observer // Listen for the events sut.emailIsValid.subscribe(emailIsValidObserver).disposed(by: disposeBag) let

    scheduler = TestScheduler(initialClock: 0) var emailIsValidObserver: TestableObserver<Bool> = scheduler.createObserver(Bool.self) it("should get an event that email is invalid") { expect(emailIsValidObserver.events.first?.value.element).to(beFalse()) }
  72. beforeEach { emailIsValidObserver = scheduler.createObserver(Bool.self) passwordIsValidObserver = scheduler.createObserver(Bool.self) signInButtonEnabledObserver =

    scheduler.createObserver(Bool.self) signInObserver = scheduler.createObserver(SignInResponse.self) scheduler.scheduleAt(0, action: { sut.emailIsValid.subscribe(emailIsValidObserver).disposed(by: disposeBag) sut.passwordIsValid.subscribe(passwordIsValidObserver).disposed(by: disposeBag) sut.signInButtonEnabled.subscribe(signInButtonEnabledObserver).disposed(by: disposeBag) sut.signInAction.subscribe(signInActionObserver).disposed(by: disposeBag) }) }
  73. beforeEach { scheduler.scheduleAt(1, action: { emailText.on(.next("test@")) }) scheduler.scheduleAt(2, action: {

    passwordText.on(.next("asd")) }) scheduler.scheduleAt(3, action: { emailText.on(.next("[email protected]")) passwordText.on(.next("Asd123!!")) }) }
  74. beforeEach { scheduler.scheduleAt(1, action: { emailText.on(.next("test@")) }) scheduler.scheduleAt(2, action: {

    passwordText.on(.next("asd")) }) scheduler.scheduleAt(3, action: { emailText.on(.next("[email protected]")) passwordText.on(.next("Asd123!!")) }) scheduler.scheduleAt(4, action: { signInButtonTap.on(.next(())) }) }
  75. beforeEach { scheduler.scheduleAt(1, action: { emailText.on(.next("test@")) }) scheduler.scheduleAt(2, action: {

    passwordText.on(.next("asd")) }) scheduler.scheduleAt(3, action: { emailText.on(.next("[email protected]")) passwordText.on(.next("Asd123!!")) }) scheduler.scheduleAt(4, action: { signInButtonTap.on(.next(())) }) scheduler.scheduleAt(5, action: { signInAPI.signInReturnValue.on(.next(SignInResponse(token: "fake_token"))) }) }
  76. beforeEach { scheduler.scheduleAt(1, action: { emailText.on(.next("test@")) }) scheduler.scheduleAt(2, action: {

    passwordText.on(.next("asd")) }) scheduler.scheduleAt(3, action: { emailText.on(.next("[email protected]")) passwordText.on(.next("Asd123!!")) }) scheduler.scheduleAt(4, action: { signInButtonTap.on(.next(())) }) scheduler.scheduleAt(5, action: { signInAPI.signInReturnValue.on(.next(SignInResponse(token: "fake_token"))) }) scheduler.start() }
  77. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabledObserver.events.first?.value.element).to(beFalse()) } it("should get an event that email is invalid") { expect(emailIsValidObserver.events.first?.value.element).to(beFalse()) } it("should get an event that password is invalid") { expect(passwordIsValidObserver.events.first?.value.element).to(beFalse()) } it("should get an event that sign in button should not be enabled") { let secondEvent = signInButtonEnabledObserver.events[1].value.element expect(secondEvent).to(beFalse()) } it("should get an event that email is valid") { let secondEvent = emailIsValidObserver.events[1].value.element expect(secondEvent).to(beTrue()) }
  78. it("should get an initial event that sign in should not

    be enabled") { expect(signInButtonEnabledObserver.events.first?.value.element).to(beFalse()) } it("should get an event that email is invalid") { expect(emailIsValidObserver.events.first?.value.element).to(beFalse()) } it("should get an event that password is invalid") { expect(passwordIsValidObserver.events.first?.value.element).to(beFalse()) } it("should get an event that sign in button should not be enabled") { let secondEvent = signInButtonEnabledObserver.events[1].value.element expect(secondEvent).to(beFalse()) } it("should get an event that email is valid") { let secondEvent = emailIsValidObserver.events[1].value.element expect(secondEvent).to(beTrue()) }
  79. it("should get an event that password is valid") { let

    secondEvent = emailIsValidObserver.events[1].value.element expect(secondEvent).to(beTrue()) } it("should get an event that sign in button should be enabled") { let secondEvent = signInButtonEnabledObserver.events[3].value.element expect(secondEvent).to(beTrue()) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } it("should give an event with the correct sign in response") { expect(signInActionObserver.events.first?.value.element?.token).to(equal(“fake_token")) }
  80. Testable Observable var emailText: TestableObservable<String> var emailText: TestableObservable<String>! = scheduler.createHotObservable(

    [.next(1, "test@"), .next(3, "[email protected]")]) // Replace Publish subject with testable observables sut.configure(emailText: emailText.asObservable(), ….
  81. beforeEach { emailText = scheduler.createHotObservable( [.next(1, "test@"), .next(3, "[email protected]")]) passwordText

    = scheduler.createHotObservable([.next(2, "asd"), .next(3, "Asd123!!")]) signInButtonTap = scheduler.createHotObservable([.next(4, ())]) // Fake the input sut.configure(emailText: emailText.asObservable(), passwordText: passwordText.asObservable(), signInButtonTap: signInButtonTap.asObservable()) }
  82. // Array of expected for the events let emailIsValidExpectedEvents: [Recorded<Event<Bool>>]

    = [ .next(1, false), .next(3, true) ] var emailText: TestableObservable<String>! = scheduler.createHotObservable( [.next(1, "test@"), .next(3, "[email protected]")]) Test time as well
  83. beforeEach { emailIsValidExpectedEvents = [ .next(1, false), .next(3, true) ]

    passwordIsValidExpectedEvents = [ .next(2, false), .next(3, true) ] signInButtonEnabledExpectedEvents = [ .next(0, false) , // start with initial value .next(2, false) , //password invalid type .next(3, false), //for valid email and invalid password type .next(3, true) //for valid email and password type ] signInActionExpectedEvents = [ .next(4, SignInResponse(token: “fake_token")), .completed(4) ] }
  84. it("should get the correct signInButtonEnabledEvents") { expect(signInButtonEnabledObserver.events).to(equal(signInButtonEnabledExpectedEvents)) } it("should get

    the correct emailIsValidExpectedEvents") { expect(emailIsValidObserver.events).to(equal(emailIsValidExpectedEvents)) } it("should get the correct passwordIsValidExpectedEvents") { expect(passwordIsValidObserver.events).to(equal(passwordIsValidExpectedEvents)) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } it("should get the correct signInActionObserver") { expect(signInActionObserver.events).to(equal(signInActionExpectedEvents)) }
  85. class SignInViewModelTestableObservableSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { //initialization code here beforeEach { signInAPI = SignInAPIFakeObservable() disposeBag = DisposeBag() sut = SignInViewModel(signInAPI: signInAPI, disposeBag: disposeBag) scheduler = TestScheduler(initialClock: 0) emailIsValidObserver = scheduler.createObserver(Bool.self) passwordIsValidObserver = scheduler.createObserver(Bool.self) signInButtonEnabledObserver = scheduler.createObserver(Bool.self) signInActionObserver = scheduler.createObserver(SignInResponse.self) emailText = scheduler.createHotObservable( [.next(1, "test@"), .next(3, "[email protected]")]) emailIsValidExpectedEvents = [ .next(1, false), .next(3, true) ] passwordText = scheduler.createHotObservable([.next(2, "asd"), .next(3, "Asd123!!")]) passwordIsValidExpectedEvents = [ .next(2, false), .next(3, true) ] signInButtonTap = scheduler.createHotObservable([.next(4, ())]) let signInResponse = SignInResponse(email: "[email protected]", account: "workable") signInAction = scheduler.createColdObservable([.next(0, signInResponse)]) signInAPI.signInReturnValue = signInAction.asObservable() signInButtonEnabledExpectedEvents = [ .next(0, false) , // start with initial value .next(2, false) , //password invalid type (didn't get in email type becasue password didn't get an initial event an //combine latest doesn't work) .next(3, false), //for valid email type .next(3, true) //for valid password type ] signInActionExpectedEvents = [ .next(4, SignInResponse(email: "[email protected]", account: "workable")), .completed(4) ] sut.configure(emailText: emailText.asObservable(), passwordText: passwordText.asObservable(), signInButtonTap signInButtonTap.asObservable(), rememberEmail: rememberEmail.asObservable()) scheduler.scheduleAt(0, action: { sut.emailIsValid.subscribe(emailIsValidObserver).disposed(by: disposeBag) sut.passwordIsValid.subscribe(passwordIsValidObserver).disposed(by: disposeBag) sut.signInButtonEnabled.subscribe(signInButtonEnabledObserver).disposed(by: disposeBag) sut.signInAction.subscribe(signInActionObserver).disposed(by: disposeBag) }) scheduler.start() } it("should get the correct signInButtonEnabledEvents") { expect(signInButtonEnabledObserver.events).to(equal(signInButtonEnabledExpectedEvents)) } it("should get the correct emailIsValidExpectedEvents") { expect(emailIsValidObserver.events).to(equal(emailIsValidExpectedEvents)) } it("should get the correct passwordIsValidExpectedEvents") { expect(passwordIsValidObserver.events).to(equal(passwordIsValidExpectedEvents)) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } it("should get the correct signInActionObserver") { expect(signInActionObserver.events).to(equal(signInActionExpectedEvents)) } } } }
  86. No nesting Easy to read ❌ We have to think

    expected events right from the start Extra flexibility to test when events are emitted ❌ We have to think timeline of events right from the start Simplifies assertions ❌ Boilerplate initialisation code
  87. Set the timeline of events ☑ by using PublishSubject ☑

    by using RxBlocking ☑ by using TestScheduler ☑ by using Marble Syntax
  88. Marble Syntax Timeline in the form `---a---b------c--|` Letters and values

    mark events in a dictionary struct: [a:1, b:true]
  89. Marble Syntax Timeline in the form `---a---b------c--|` Returns: Observable sequence

    specified by timeline and values. Letters and values mark events in a dictionary struct: [a:1, b:true]
  90. class SignInViewModelMarbleSyntaxSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { // <initial properties here> var emailText: TestableObservable<String>! var passwordText: TestableObservable<String>! var signInButtonTap: TestableObservable<Void>! var signIn: TestableObservable<SignInResponse>! var emailIsValidObserver: TestableObserver<Bool>! var passwordIsValidObserver: TestableObserver<Bool>! var signInButtonEnabledObserver: TestableObserver<Bool>! var signInObserver: TestableObserver<SignInResponse>! var emailIsValidExpectedEvents: [Recorded<Event<Bool>>]! var passwordIsValidExpectedEvents: [Recorded<Event<Bool>>]! var signInButtonEnabledExpectedEvents: [Recorded<Event<Bool>>]! var signInExpectedEvents: [Recorded<Event<SignInResponse>>]! } } } Same properties as before Different initialization
  91. Marble Syntax let emails = [ "e1": “test@", "e2": “[email protected]"]

    Letters and values mark events in a dictionary struct: [a:1, b:true]
  92. Marble Syntax let emails = [ "e1": “test@", "e2": “[email protected]"]

    let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails) Timeline in the form `---a---b------c--|` Letters and values mark events in a dictionary struct: [a:1, b:true]
  93. Marble Syntax Returns: Observable sequence specified by timeline and values.

    let emails = [ "e1": “test@", "e2": “[email protected]"] let emailText = scheduler.createHotObservable(emailEvents) let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails) Timeline in the form `---a---b------c--|` Letters and values mark events in a dictionary struct: [a:1, b:true]
  94. Marble Syntax Returns: Observable sequence specified by timeline and values.

    let emails = [ "e1": “test@", "e2": “[email protected]"] let emailText = scheduler.createHotObservable(emailEvents) let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails) Timeline in the form `---a---b------c--|` Letters and values mark events in a dictionary struct: [a:1, b:true]
  95. Marble Syntax Returns: Observable sequence specified by timeline and values.

    let emails = [ "e1": “test@", "e2": “[email protected]"] let emailText = scheduler.createHotObservable(emailEvents) let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails) Timeline in the form `---a---b------c--|` Letters and values mark events in a dictionary struct: [a:1, b:true]
  96. let emails = [ "e1": “test@", "e2": “[email protected]"] let emailEvents

    = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails).first! let emailText = scheduler.createHotObservable(emailEvents) sut.configure(emailText: emailText.asObservable() …
  97. beforeEach { // input events let emails = [ "e1":

    “test@", "e2": "[email protected]"] let passwords = ["p1": "asd", "p2": "Asd123!!"] let signInButtonTap = ["s1": ()] let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails).first! let passwordEvents = scheduler.parseEventsAndTimes(timeline: "-----p1-----p2-----", values: passwords).first! let signInButtonTapEvents = scheduler.parseEventsAndTimes(timeline: "---------------------------s1-", values: signInButtonTap).first! }
  98. beforeEach { // input events let emails = [ "e1":

    “test@", "e2": "[email protected]"] let passwords = ["p1": "asd", "p2": "Asd123!!"] let signInButtonTap = ["s1": ()] let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails).first! let passwordEvents = scheduler.parseEventsAndTimes(timeline: "-----p1-----p2-----", values: passwords).first! let signInButtonTapEvents = scheduler.parseEventsAndTimes(timeline: "---------------------------s1-", values: signInButtonTap).first! // expected events let emailIsValid = ["v1": false, "v2": true] let passwordIsValid = ["u1" :false, "u2": true] let signInButtonEnabled = ["b1": false, "b2": false, "b3": false, "b4" :true] let signInResponseEvents = ["x1": SignInResponse(email: "[email protected]", account: “workable")] expectedEmailIsValidEvents = scheduler.parseEventsAndTimes(timeline: "---v1----v2-----", values: emailIsValid).first! expectedPasswordIsValidEvents = scheduler.parseEventsAndTimes(timeline: "-----u1-----u2-----", values: passwordIsValid) .first! signInEnabledExpectedEvents = scheduler.parseEventsAndTimes(timeline: "b1---b2--b3-b4--", values: signInButtonEnabled) .first! signInExpectedEvents = scheduler.parseEventsAndTimes(timeline: "---------------------------------x1-", values: signInResponseEvents).first! }
  99. it("should get the correct signInButtonEnabledEvents") { expect(signInButtonEnabledObserver.events).to(equal(signInEnabledExpectedEvents)) } it("should get

    the correct emailIsValidExpectedEvents") { expect(emailIsValidObserver.events).to(equal(expectedEmailIsValidEvents)) } it("should get the correct passwordIsValidExpectedEvents") { expect(passwordIsValidObserver.events).to(equal(expectedPaswordIsValidEvents)) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } it("should get the correct signInActionObserver") { expect(signInActionObserver.events.first).to(equal(signInActionExpectedEvents.first)) }
  100. class SignInViewModelMarbleSyntaxSpec: QuickSpec { override func spec() { describe("The SignInViewModel")

    { //initialization code here beforeEach { signInAPI = SignInAPIFake() disposeBag = DisposeBag() sut = SignInViewModel(signInAPI: signInAPI, disposeBag: disposeBag) scheduler = TestScheduler(initialClock: 0) emailIsValidObserver = scheduler.createObserver(Bool.self) passwordIsValidObserver = scheduler.createObserver(Bool.self) signInObserver = scheduler.createObserver(SignInResponse.self) let emails = ["e1": "test@", "e2": "[email protected]"] let passwords = ["p1": "asd", "p2": "Asd123!!"] let rememberEmailEvent = ["r1": false] let signInButtonClick = ["s1": ()] let emailEvents = scheduler.parseEventsAndTimes(timeline: "---e1----e2-----", values: emails).first! let passwordEvents = scheduler.parseEventsAndTimes(timeline: "-----p1-----p2-----", values: passwords).first! let signInButtonTapEvents = scheduler.parseEventsAndTimes(timeline: "---------------------------s1-", values: signInButtonClick).first! let emailIsValid = ["v1": false, "v2": true] let passwordIsValid = ["u1" :false, "u2": true] let signInButtonEnabled = ["b1": false, "b2": false, "b3": false, "b4" :true] let signInResponseEvents = ["x1": SignInResponse(email: "[email protected]", account: "workable")] expectedEmailIsValidEvents = scheduler.parseEventsAndTimes(timeline: "---v1----v2-----", values: emailIsValid).first! expectedPaswordIsValidEvents = scheduler.parseEventsAndTimes(timeline: "-----u1-----u2-----", values: passwordIsValid).first! signInEnabledExpectedEvents = scheduler.parseEventsAndTimes(timeline: "b1---b2--b3-b4--", values: signInButtonEnabled).first! signInExpectedEvents = scheduler.parseEventsAndTimes(timeline: "---------------------------------x1-", values: signInResponseEvents).first! emailText = scheduler.createHotObservable( emailEvents) passwordText = scheduler.createHotObservable(passwordEvents) signInButtonTap = scheduler.createHotObservable(signInButtonTapEvents) sut.configure(emailText: emailText.asObservable(), passwordText: passwordText.asObservable(), signInButtonTap: signInButtonTap.asObservable()) sut.emailIsValid.subscribe(emailIsValidObserver).disposed(by: disposeBag) sut.passwordIsValid.subscribe(passwordIsValidObserver).disposed(by: disposeBag) sut.signInButtonEnabled.subscribe(signInButtonEnabledObserver).disposed(by: disposeBag) sut.signIn.subscribe(signInActionObserver).disposed(by: disposeBag) scheduler.scheduleAt(33, action: { signInAPI.signInReturnValue.on(.next(SignInResponse(email: "[email protected]", account: "workable"))) }) scheduler.start() } it("should get the correct signInButtonEnabledEvents") { expect(signInButtonEnabledObserver.events).to(equal(signInEnabledExpectedEvents)) } it("should get the correct emailIsValidExpectedEvents") { expect(emailIsValidObserver.events).to(equal(expectedEmailIsValidEvents)) } it("should get the correct passwordIsValidExpectedEvents") { expect(passwordIsValidObserver.events).to(equal(expectedPaswordIsValidEvents)) } it("a sign in request should be made") { expect(signInAPI.signInCalled).to(beTrue()) } it("should get the correct signInActionObserver") { expect(signInObserver.events.first).to(equal(signInExpectedEvents.first)) } } } }
  101. ❌ Difficult to read No nesting ❌ We have to

    think expected events right from the start Extra flexibility to test when events are emitted ❌ We have to think timeline of events right from the start ❌ Difficult to write
  102. Think about it this way: Then, you assert your output

    to make sure that it emits the expected events at the right times. You fake a stream of events and feed it into your view model’s input
  103. Conclusions Different ways to unit test a component Each one

    has pros & ❌ cons Select the one that suits your needs