tofucodes diary

にほんごのほう

XCTestでFirebase Realtime DatabaseのAPIをモックしてレスポンスを偽造する

仕事でFirebase Realtime Databaseを利用していてユニットテストを書くためにモックについて調べてみるとこちらの記事に出会いました。

medium.com

モック以外の内容も書かれており素晴らしい内容ですね。とても参考になります。

今回はもう少しお手軽に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から指定したデータが返却されているように偽造することできるようになります。