swiftunit-testingrx-swiftxctestrxtest

RxSwift/RxTest How to test async function with Observable return


I'm quite new to RxSwift and I trying to create some unit tests. In this case I want to test If the fetch objects from Realtime database Firebase is occurring correctly.

func getAllPosts() -> Observable<[PostItem]> {
      ref = Database.database().reference()
        return Observable.create { observer -> Disposable in
        
        self.ref.child("Posts").observe(.value) { (snapshot) in
            var postsList:[PostItem] = []
            for child in snapshot.children {
                let snap = child as! DataSnapshot
                
            let postDict = snap.value as! [String: Any]
                let postAux = PostItem(id: snap.ref.key ?? "", authorId: postDict["authorId"] as? String  ?? "", name: postDict["name"] as? String  ?? "", content: postDict["content"] as? String  ?? "", createAt: postDict["createAt"] as? String  ?? "")
            postsList.append(postAux)
            }
            observer.onNext(postsList)
        }
        
    return Disposables.create {}
    }
}

The problem is the return of firebase is async and the way i'm trying the test is being completed before the return.

func testFetchPosts() throws {
    
    let newsAPIService = NewsAPIService()
    let posts = newsAPIService.fetchNewsFromAPI()
    
    XCTAssertNil(posts, "The posts is nil")
}

Obs: I tried to use XCTest expectation but I don't know if had implemented incorrectly or if it doesn't really work

Thank you!


Solution

  • I suggest that you move the logic out of the Observable.create so you can test it independently. Something like this:

    func getAllPosts() -> Observable<[PostItem]> {
        return Observable.create { observer in
            // note that you don't have to store this value. The disposable will hold it for you.
            let ref = Database.database().reference()
            // the `handleSnapshot(observer:)` function is defined below.
            ref.child("Posts").observe(.value, handleSnapshot(observer: observer))
            // make sure you turn off the observation on dispose.
            return Disposables.create { ref.stop() }
        }
    }
    

    Your logic should be in a separate, higher-order, function:

    // note that this is a free function. It should not be defined inside any class or struct.
    // I'm guessing on the `Snapshot` type. I don't use Firebase.
    func handleSnapshot(observer: AnyObserver<[PostItem]>) -> (Snapshot) -> Void {
        { snapshot in
            var postsList: [PostItem] = []
            for child in snapshot.children {
                let snap = child as! DataSnapshot
    
                let postDict = snap.value as! [String: Any]
                let postAux = PostItem(id: snap.ref.key ?? "", authorId: postDict["authorId"] as? String  ?? "", name: postDict["name"] as? String  ?? "", content: postDict["content"] as? String  ?? "", createAt: postDict["createAt"] as? String  ?? "")
                postsList.append(postAux)
            }
            observer.onNext(postsList)
        }
    }
    

    Testing this function is easy:

    func testShapshotHandler() throws {
        let scheduler = TestScheduler(initialClock: 0)
        let observer = scheduler.createObserver([PostItem].self)
        let snapshot = Snapshot(children: [Child()]) // create a snapshot object here
        let sut = handleSnapshot(observer: observer.asObserver())
    
        sut(snapshot)
    
        // assumes `PostItem` is Equatable
        XCTAssertEqual(observer.events, [.next(0, [PostItem(id: "", authorId: "", name: "", content: "", createAt: "")])])
    }
    

    If you are uncomfortable with higher-order functions, you could do something like this:

    func getAllPosts() -> Observable<[PostItem]> {
        return Observable.create { observer in
            let ref = Database.database().reference()
            ref.child("Posts").observe(.value) { snapshot in
                observer.onNext(handle(snapshot: snapshot))
            }
            return Disposables.create { ref.stop() }
        }
    }
    
    func handle(snapshot: Snapshot) -> [PostItem] {
        var postsList: [PostItem] = []
        for child in snapshot.children {
            let snap = child as! DataSnapshot
    
            let postDict = snap.value as! [String: Any]
            let postAux = PostItem(id: snap.ref.key ?? "", authorId: postDict["authorId"] as? String  ?? "", name: postDict["name"] as? String  ?? "", content: postDict["content"] as? String  ?? "", createAt: postDict["createAt"] as? String  ?? "")
            postsList.append(postAux)
        }
        return postsList
    }
    

    And now testing the logic is even easier. You don't need a test scheduler or observer anymore.