XCTestでFirebase Realtime DatabaseのAPIをモックしてレスポンスを偽造する
仕事でFirebase Realtime Databaseを利用していてユニットテストを書くためにモックについて調べてみるとこちらの記事に出会いました。
モック以外の内容も書かれており素晴らしい内容ですね。とても参考になります。
今回はもう少しお手軽にFirebase Realtime Databaseのレスポンスをモックする方法を考えてみました。
複雑なことせずになんとかならんのか、という方のお役に立てれば何よりです。
テスト対象コード(サンプル)
class FirebaseDatabaseClient { private let databaseReference: DatabaseReference init(with databaseReference: DatabaseReference) { self.databaseReference = databaseReference } func readSample(completion: @escaping ([String]) -> Void) { self.databaseReference .child("sample") .observeSingleEvent(of: .value) { snapshot in var sampleList: [String] = [] // 省略... completion(sampleList) } } }
- DatabaseReferenceのインスタンスをコンストラクタで受け取る
- "sample"というパスのデータを
observeSingleEvent
で1度だけ取得 - レスポンスのsnapshotをごにょごにょしてcompletionハンドラに引き渡し実行
といったような実装になっています。
テストコード
import XCTest import FirebaseDatabase @testable import Sample_App class FirebaseDatabaseClientTests: XCTestCase { func testReadSample() { let client = FirebaseDatabaseClient(with: MockDatabaseReference()) let expectaion = expectation(description: #function) client.readSample { sampleList in XCTAssertTrue(sampleList.contains("something")) expectaion.fulfill() } wait(for: [expectaion], timeout: 0.5) } } // MARK: - Override Firebae Realtime Database private class MockDatabaseReference: DatabaseReference { override func child(_ pathString: String) -> DatabaseReference { return self } override func observeSingleEvent(of eventType: DataEventType, with block: @escaping (DataSnapshot) -> Void) { let snapshot = MockSnapshot() DispatchQueue.global().async { block(snapshot) } } } private class MockSnapshot: DataSnapshot { override var value: Any? { return ["key_1": "value_1", "key_2": "value_2"] } }
1つ1つ軽く解説します。
テストケース
func testReadSample() { let client = FirebaseDatabaseClient(with: MockDatabaseReference()) let expectaion = expectation(description: #function) client.readSample { sampleList in XCTAssertTrue(sampleList.contains("something")) expectaion.fulfill() } wait(for: [expectaion], timeout: 0.5) }
まずはテストケースから。1行目でテスト対象のクラスを作成しますが、その際に引数としてFirebaseのDatabaseReferenceクラスのモックオブジェクトを渡します。モックオブジェクト自体の解説は後述します。
またデータベースからのデータ取得は非同期であるはずなのでXCTestExpectation
を用います。
DatabaseReferenceのモック
private class MockDatabaseReference: DatabaseReference { override func child(_ pathString: String) -> DatabaseReference { return self } override func observeSingleEvent(of eventType: DataEventType, with block: @escaping (DataSnapshot) -> Void) { let snapshot = MockSnapshot() DispatchQueue.global().async { block(snapshot) } } }
こちらが先ほども出てきたFirebaseのRealtimeDatabaseのモックオブジェクトの定義です。今回はテスト対象のクラスでobserveSingleEvent
を利用しているのでその関数と、child
関数もオーバーライドしています。
child
関数をオーバーライドする理由としては、アプリコードの方でデータベースの特定のパスのデータを取得する場合にchild("path")
というAPIが実行されると、テスト対象クラスのコンストラクタに引き渡したDatabaseReferenceオブジェクトとは別のインスタンスが使われてしまってモックすることができません。
なので今回はchild
が何度実行されようともモックオブジェクトを返すようにして、どんなpathのデータを取得しようともモックのDatabaseReferenceクラスが利用されるようにしています。もしpathによって返却するデータや処理を変えたい場合は、以下のようにオーバライドメソッドの中でpathString
を見て別のオブジェクトを返却したりとかでできるかもしれません。
override func child(_ pathString: String) -> DatabaseReference { switch (pathString) { case "hoge": return HogeDatabaseReference() case "fuga": return FugaDatabaseReference() } }
DataSnapshotのモック
private class MockSnapshot: DataSnapshot { override var value: Any? { return ["key_1": "value_1", "key_2": "value_2"] } }
最後に実際のレスポンスのクラスであるDataSnapshotのモックオブジェクトの定義です。DataSnapshotのvalue
プロパティは本来readonlyなのでそのままではvalue
を自分好みにすることができません。そこでvalue
のプロパティをオーバーライドして、テストで利用したいデータを返却します。
このモックオブジェクトを先ほどのDatabaseReferenceクラスのモックオブジェクトのobserveSingleEvent
で利用することで、Realtime Databaseから指定したデータが返却されているように偽造することできるようになります。