はじめに
Modelクラスのテストを書くにあたっての備忘録としてまとめました。
今回は例としてCoreLocationを用いたServiceクラスを想定しています。
こちらの記事を参考にさせていただきました。
前提
Modelクラス自体の実装
以下のような実装の想定です。
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を定義します。
protocol CLLocationManagerProtocol {
var delegate: CLLocationManagerDelegate? { get set }
func requestWhenInUseAuthorization()
func requestLocation()
}
extension CLLocationManager: CLLocationManagerProtocol {}
上記を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クラスを利用して、テストを書いてみます。
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の恩恵を感じます。
指摘やアドバイスがあればお願いします!