2

I am trying to add tests to an existing code base.

The app uses RxSwift w/ a coordinator pattern.

The AppCoordinator looks like -

import UIKit
import RxSwift

enum AuthenticationState {
    case unknown, signedIn, signedOut
}

final class AppCoordinator: BaseCoordinator<Void> {
    private let window: UIWindow

    init(window: UIWindow) {
        self.window = window
    }

    override func start() -> Observable<Void> {
        coordinateToRoot(basedOn: .unknown)
        return .never()
    }

    /// Recursive method that will restart a child coordinator after completion.
    /// Based on:
    /// https://github.com/uptechteam/Coordinator-MVVM-Rx-Example/issues/3
    private func coordinateToRoot(basedOn state: AuthenticationState) {

        switch state {

        case .unknown:
            return showStartScene()
                .subscribe(onNext: { [weak self] authState in
                    self?.window.rootViewController = nil
                    self?.coordinateToRoot(basedOn: authState)
                })
                .disposed(by: disposeBag)

        case .signedOut:
            return showAuthenticationFlow()
                .subscribe(onNext: { [weak self] authState in
                    self?.window.rootViewController = nil
                    self?.coordinateToRoot(basedOn: authState)
                })
                .disposed(by: disposeBag)

        case .signedIn:
            return startHomeFlow()
                .subscribe(onNext: { [weak self] authState in
                    self?.window.rootViewController = nil
                    self?.coordinateToRoot(basedOn: authState)
                })
                .disposed(by: disposeBag)
        }

    }

    private func showStartScene() -> Observable<AuthenticationState> {
        let coordinator = StartCoordinator(window: window)
        return coordinate(to: coordinator).map { _ in return dependencies.authSvc.authState }
    }

    private func showAuthenticationFlow() -> Observable<AuthenticationState> {
        let coordinator = AuthCoordinator(window: window)
        return coordinate(to: coordinator).map { _ in return dependencies.authSvc.authState }
    }

    private func startHomeFlow() -> Observable<AuthenticationState> {
        let coordinator = HomeCoordinator(window: window)
        return coordinate(to: coordinator).map { _ in return dependencies.authSvc.authState }
    }
}

The flow is as follows - start invokes the showStartScene method. This will check if a token exists and if the user should see the onboarding screen. Once everthing is complete it emits an event that causes coordinateToRoot to be called, using the latest auth state to direct the user to the correct Coordinator

I have been able to add an initial test to assert that window.rootViewController is set on start, however I would like assert on the behaviour around coordinateToRoot

class AppCoordinatorTests: XCTestCase {
    var sut: AppCoordinator!
    var window: UIWindow!
    var disposeBag: DisposeBag!

    override func setUp() {
        super.setUp()
        window = UIWindow(frame: .zero)
        sut = AppCoordinator(window: window)
        disposeBag = DisposeBag()
    }

    override func tearDown() {
        super.tearDown()
        window = nil
        sut = nil
        disposeBag = nil
    }

    func test_on_start_root_view_is_not_nil() {
        sut.start()
        .subscribe()
        .disposed(by: disposeBag)

        XCTAssertNotNil(window.rootViewController)
    }
}

Each coordinator emits Void and then uses a map on the current auth service to call a recursive method and invoke the next, valid coordinator.

I was thinking perhaps I emit an event on the current coordinator, having preset the result of dependencies.authSvc.authState I could then assert on the type of UIViewController set as window.rootViewController

eg -

XCTAssertTrue((window.rootViewController as Any) is SomeTypeOfViewController)

What I would like to know is what would make this a more testable class without simply making everything public.

I have considered using dependency injection to setup the child coordinators and then using a stub to mock events, I am unsure if this is the best option in the context of something like RxSwift

Tim J
  • 1,211
  • 1
  • 14
  • 31

0 Answers0