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