1

I got this class which I'd like to write tests for:

import CoreLocation
import RxCocoa
import RxSwift

struct LocationManager {

    private (set) var authorized: Driver<Bool>
    private let coreLocationManager = CLLocationManager()

    init() {
        coreLocationManager.distanceFilter = kCLDistanceFilterNone
        coreLocationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation

        authorized = Observable.deferred { [weak coreLocationManager] in
            let status = CLLocationManager.authorizationStatus()
            guard let coreLocManager = coreLocationManager else {
                return Observable.just(status)
            }
            return coreLocManager
                .rx_didChangeAuthorizationStatus
                .startWith(status)
            }
            .asDriver(onErrorJustReturn: CLAuthorizationStatus.NotDetermined)
            .map {
                switch $0 {
                case .AuthorizedWhenInUse:
                    return true
                default:
                    return false
                }
        }

        coreLocationManager.requestWhenInUseAuthorization()
    }
}

Basically I want to test whether the authorized Driver has the correct value based on possible CLAuthorizationStatuses. I need a hint in the right direction since I am not familiar with unit testing with RxSwift. I guess my best option is to create a mock of CLLocationManager which returns some CLAuthorizationStatus when authorizationStatus() is called and afterwards I would check the value of the authorized Driver right ?

Any explanation on how to test this LocationManager class is appreciated.

dehlen
  • 7,325
  • 4
  • 43
  • 71

2 Answers2

2

OK I got it working. However I am not sure if this is a valid solution. Feel free to correct me here.

First of all I changed my LocationManager class to this:

import CoreLocation
import RxCocoa
import RxSwift

struct LocationManager<T where T: LocationManagerProtocol> {

    private (set) var authorized: Driver<Bool>
    private let coreLocationManager = CLLocationManager()

    init(type: T.Type) {
        coreLocationManager.distanceFilter = kCLDistanceFilterNone
        coreLocationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation

        authorized = Observable.deferred { [weak coreLocationManager] in
            let status = type.authorizationStatus()
            guard let coreLocManager = coreLocationManager else {
                return Observable.just(status)
            }
            return coreLocManager
                .rx_didChangeAuthorizationStatus
                .startWith(status)
            }
            .asDriver(onErrorJustReturn: CLAuthorizationStatus.NotDetermined)
            .map {
                switch $0 {
                case .AuthorizedWhenInUse:
                    return true
                default:
                    return false
                }
        }

        coreLocationManager.requestWhenInUseAuthorization()
    }
}

Basically it is possible now to provide a type conforming to a new protocol I wrote LocationManagerProtocol.

Next the protocol implements the authorizationStatus function so I can mock this one.

protocol LocationManagerProtocol {
    static func authorizationStatus() -> CLAuthorizationStatus
}

Then I created an extension to CLLocationManager to implement this protocol:

import CoreLocation

extension CLLocationManager: LocationManagerProtocol {

}

Call from production code: let locationManager = LocationManager(type: CLLocationManager.self)

Call from test code: let locationManager = LocationManager(type: AuthorizedLocationManager.self) or let locationManager = LocationManager(type: ForbiddenLocationManager.self)

In my test class I added these two classes two override the authorizationStatus method:

class AuthorizedLocationManager: LocationManagerProtocol {
    static func authorizationStatus() -> CLAuthorizationStatus {
        return .AuthorizedWhenInUse
    }
}

class ForbiddenLocationManager: LocationManagerProtocol {
    static func authorizationStatus() -> CLAuthorizationStatus {
        return .Denied
    }
}

My test cases:

func testLocationAuthorizationPermitted() {
    let locationManager = LocationManager(type:  AuthorizedLocationManager.self)

    locationManager.authorized
    .driveNext { authorized in
        XCTAssertTrue(authorized)
    }
    .addDisposableTo(disposeBag)
}

func testLocationAuthorizationRejected() {
    let locationManager = LocationManager(type: ForbiddenLocationManager.self)

    locationManager.authorized
        .driveNext { authorized in
            XCTAssertFalse(authorized)
        }
        .addDisposableTo(disposeBag)
}

Sorry for the long post. I thought I would give you all information on how I solved this now. However as I stated earlier I am new to this and would love to hear other opinions on this approach and whether this is a valid test scenario or not.

dehlen
  • 7,325
  • 4
  • 43
  • 71
1

Take a look at how RxSwift implements unit tests: https://github.com/ReactiveX/RxSwift/blob/4b3056b81f619c0bf12c6ee4e571582219e2b88d/Tests/RxSwiftTests/Tests/Observable%2BSingleTest.swift

As for using a mock of CLLocationManager, yea, dependency injection would be the way to go.

solidcell
  • 7,639
  • 4
  • 40
  • 59