5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

5
Last updated at Posted at 2019-04-09

はじめに

Modelクラスのテストを書くにあたっての備忘録としてまとめました。
今回は例としてCoreLocationを用いたServiceクラスを想定しています。
こちらの記事を参考にさせていただきました。

前提

Modelクラス自体の実装

以下のような実装の想定です。

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を定義します。

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

extension CLLocationManager: CLLocationManagerProtocol {}

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

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クラスを利用して、テストを書いてみます。

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の恩恵を感じます。
指摘やアドバイスがあればお願いします!

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?