【Swift】Protocolを用いて標準フレームワークに依存したModelクラスをテストする

# はじめに
Modelクラスのテストを書くにあたっての備忘録としてまとめました。
今回は例としてCoreLocationを用いたServiceクラスを想定しています。
[こちら](https://medium.com/flawless-app-stories/solving-dependencies-in-swift-9ee6ad4a8941)の記事を参考にさせていただきました。

# 前提
## Modelクラス自体の実装
以下のような実装の想定です。

```swift:LocationService.swift

protocol LocationServiceInputProtocol: AnyObject {
    var output: LocationServiceOutputProtocol? { get set }

    func setup()
}

protocol LocationServiceOutputProtocol: AnyObject {
    func didUpdateLocations(_ latitude: Double, _ longitude: Double)
    func didFail(error: Error)
}

final class LocationService: NSObject, LocationServiceInputProtocol {

    var output: LocationServiceOutputProtocol?
    
    private var latitude: Double?
    private var longitude: Double?
    
    init() {
        self.locationManager = CLLocationManager()
    }
    
    func setup() {
        self.locationManager.delegate = self
    }
}

extension LocationService: CLLocationManagerDelegate {
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            self.locationManager.requestWhenInUseAuthorization()
        case .denied: break
        case .restricted: break
        case .authorizedAlways: break
        case .authorizedWhenInUse:
            self.locationManager.requestLocation()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = manager.location {
            self.output?.didUpdateLocations(location.coordinate.latitude, location.coordinate.longitude)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        self.output?.didFail(error: error)
    }
}
```


## 問題点
上記のコードの`locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)`における、
**`status`が`.notDetermined`の時に`requestWhenInUseAuthorization`を呼んでいるか**のテストの仕方についてです。
CLLocationManagerの特定メソッドが呼ばれたことのテストをProtocolを用いて実現します。

# 実装
## CLLocationManagerProtocol
命名が短絡的でアレですが。。
`requestWhenInUseAuthorization`が呼ばれているかの確認をするためにはCLLocationManagerのMockクラスを作成したいです。
そのために、Protocolを定義します。

```swift:CLLocationManager+TestableProtocol
protocol CLLocationManagerProtocol {
    var delegate: CLLocationManagerDelegate? { get set }
    
    func requestWhenInUseAuthorization()
    func requestLocation()
}

extension CLLocationManager: CLLocationManagerProtocol {}
```

上記をLocationServiceに定義し、外部から注入出来るようにします。

```swift:LocationService
protocol LocationServiceInputProtocol: AnyObject {
    var output: LocationServiceOutputProtocol? { get set }
    var locationManager: CLLocationManagerProtocol { get }

    func setup()
}

...

final class LocationService: NSObject, LocationServiceInputProtocol {

    var output: LocationServiceOutputProtocol?
    private(set) var locationManager: CLLocationManagerProtocol
    
    private var latitude: Double?
    private var longitude: Double?
    
    // MARK: - Initialize
    
    init(locationManager: CLLocationManagerProtocol) {
        self.locationManager = locationManager
    }

    ...

}
```

これで、CLLocationManagerのMockクラスを渡せばテストすることが出来そうです。

## テスト
定義したProtocolを用いたMockクラスを利用して、テストを書いてみます。

```swift:LocationServiceTests.swift
class LocationServiceTests: XCTestCase {
    
    private let locationManager = MockLocationManager()
    private var locationService: LocationService!

    override func setUp() {
        super.setUp()
        
        self.locationService = LocationService(locationManager: self.locationManager)
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }
    
    // MARK: - Tests
    
    func test_locationManagerDidChangeAuthorization() {
        class FakeLocationManager: CLLocationManager {}
        
        let beforeCallCountRequestWhenInUseAuthorization = self.locationManager.callCountRequestWhenInUseAuthorization
        self.locationService.locationManager(FakeLocationManager(), didChangeAuthorization: .notDetermined)
        XCTContext.runActivity(named: "LocationManagerのstatusがnotDeterminedの時、requestWhenInUseAuthorizationが呼ばれているか") { _ in
            XCTAssertEqual(beforeCallCountRequestWhenInUseAuthorization + 1, self.locationManager.callCountRequestWhenInUseAuthorization)
        }
        
        let beforeCallCountRequestLocation = self.locationManager.callCountRequestLocation
        self.locationService.locationManager(FakeLocationManager(), didChangeAuthorization: .authorizedWhenInUse)
        XCTContext.runActivity(named: "LocationManagerのstatusがauthorizedWhenInUseの時、requestLocationが呼ばれているか") { _ in
            XCTAssertEqual(beforeCallCountRequestLocation + 1, self.locationManager.callCountRequestLocation)
        }
    }
    
    // MARK: - Mock

    class MockLocationManager: CLLocationManagerProtocol {
        weak var delegate: CLLocationManagerDelegate?
        
        var callCountRequestWhenInUseAuthorization = 0
        var callCountRequestLocation = 0
        
        func requestWhenInUseAuthorization() {
            self.callCountRequestWhenInUseAuthorization += 1
        }
        
        func requestLocation() {
            self.callCountRequestLocation += 1
        }
    }

}
```
テスト出来ました！

# おわりに
やってみれば複雑なことをしているわけでもないですが、中々解決出来ずテストはナレッジが大事だなと感じました。
こういうことがパッと出来るのもSwiftの恩恵を感じます。
指摘やアドバイスがあればお願いします！