groue / CombineExpectations

Utilities for tests that wait for Combine publishers
MIT License
252 stars 18 forks source link

Recorder does not receive elements emitted by a Realm FrozenCollection publisher #10

Closed rustproofFish closed 3 years ago

rustproofFish commented 4 years ago

I'm tinkering with persistence middleware for an app which uses Redux-like architecture using Realm as a backend. Results from queries on, or changes to the database are emitted by a Combine Publisher.

Although Realm now supports Combine and provides FrozenCollection types to manage threading and concurrency-related errors, I've been advised to use DTOs (structs) to make things as predictable as possible. I've written a test which utilises CombineExpectations to ensure that the conversion of the Realm FrozenCollection to the DTO behaves as expected but the Recorder doesn't receive any elements within the time frame set (1 second). I wondered initially if this was due to the use of .receive(on: DispatchQueue.main) in my operator chain but commenting this out didn't resolve the issue.

Using a "vanilla" Expectation, things works as expected and I've inspected the Publisher using print() (6 elements emitted in total, conversion to DTO working OK) so am working on the assumption that I am missing something although I guess there is the outside possibility there's a glitch somewhere as this is effectively an integration test...?

Currently using Xcode12, iOS14. Code as follows:

Middleware (system under test)

 private var cancellables = Set<AnyCancellable>()
    private var subject = CurrentValueSubject<[ToDo.DTO], Never>([])

    func allToDos(in realm: Realm = try! Realm()) -> AnyPublisher<[ToDo.DTO], Never> {
        realm.objects(ToDo.self)
            .collectionPublisher
//            .print()
            .assertNoFailure()
            .freeze()
            .map { item in
                item.map { $0.convertToDTO() }
            }
//            .print()
//            .receive(on: DispatchQueue.main)
            .subscribe(subject) // using a Subject to facilitate pipeline sharing and buffering of last output
            .store(in: &cancellables)

        return subject.eraseToAnyPublisher()

    }

    deinit {
        cancellables = []
    }
}

Test class

class SwiftRex_ToDo_PersistedTests: XCTestCase {
    private var realm: Realm?

    private var testSet1: [ToDo] {
        [
            ToDo(name: "Mow lawn"),
            ToDo(name: "Wash car"),
            ToDo(name: "Clean windows")
        ]
    }

    private var testSet2: [ToDo] {
        [
            ToDo(name: "Walk dog"),
            ToDo(name: "Cook dinner"),
            ToDo(name: "Pay bills")
        ]
    }

    override func setUp()  {
        // Put setup code here. This method is called before the invocation of each test method in the class.
        realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: UUID().uuidString))
    }

    override func tearDown()  {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        realm = nil
    }

    func testCorrectNumberOfObjectsStoredInRealm() {
        realm!.addForTesting(objects: testSet1)
        XCTAssertEqual(realm!.objects(ToDo.self).count, 3)

        realm!.addForTesting(objects: testSet2)
        XCTAssertEqual(realm!.objects(ToDo.self).count, 6)
    }

    func testMiddlewarePublisherUsingCombineExpectationsFramework() {
        let sut = PersistenceMiddleware().allToDos(in: realm!)
        let recorder = sut.record()

        realm!.addForTesting(objects: testSet1)
        realm!.addForTesting(objects: testSet2)

        var elements = [ToDo.DTO]()
        do {
            elements = try wait(for: recorder.prefix(2), timeout: 1).flatMap { $0 }
        } catch {
            XCTFail("Operation failed with error \(error)")
        }
        XCTAssertEqual(elements.count, 6, "Expected 6 elements, received \(elements.count)")
    }

    func testMiddlewarePublisherUsingExpectation() {
        let middleware = PersistenceMiddleware()
        var cancellables = Set<AnyCancellable>()
        let receivedValues = expectation(description: "received expected number of published objects")

        middleware.allToDos(in: realm!)
            .sink { result in
                if result.count == 6 {
                    NSLog(result.debugDescription)
                    receivedValues.fulfill()
                }
            }
            .store(in: &cancellables)

        realm!.addForTesting(objects: testSet1)
        realm!.addForTesting(objects: testSet2)

        waitForExpectations(timeout: 1, handler: nil)
    }

}

Test output (test using CombineExpectations fails - other tests pass)

Test Suite 'All tests' started at 2020-10-04 13:23:16.699 Test Suite 'SwiftRex-ToDo-PersistedTests.xctest' started at 2020-10-04 13:23:16.700 Test Suite 'SwiftRex_ToDo_PersistedTests' started at 2020-10-04 13:23:16.700 Test Case '-[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testCorrectNumberOfObjectsStoredInRealm]' started. Test Case '-[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testCorrectNumberOfObjectsStoredInRealm]' passed (0.060 seconds). Test Case '-[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testMiddlewarePublisherUsingCombineExpectationsFramework]' started. 2020-10-04 13:23:16.790733+0100 SwiftRex-ToDo-Persisted[24035:1350643] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed /Users/adrianward/Documents/Xcode/SwiftRex-ToDo-Persisted/SwiftRex-ToDo-PersistedTests/SwiftRex_ToDo_PersistedTests.swift:62: error: -[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testMiddlewarePublisherUsingCombineExpectationsFramework] : Asynchronous wait failed: Exceeded timeout of 1 seconds, with unfulfilled expectations: "". /Users/adrianward/Documents/Xcode/SwiftRex-ToDo-Persisted/SwiftRex-ToDo-PersistedTests/SwiftRex_ToDo_PersistedTests.swift:66: error: -[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testMiddlewarePublisherUsingCombineExpectationsFramework] : XCTAssertEqual failed: ("0") is not equal to ("6") - Expected 6 elements, received 0 Test Case '-[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testMiddlewarePublisherUsingCombineExpectationsFramework]' failed (1.048 seconds). Test Case '-[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testMiddlewarePublisherUsingExpectation]' started. 2020-10-04 13:23:17.814079+0100 SwiftRex-ToDo-Persisted[24035:1350455] [SwiftRex_ToDo_Persisted.ToDo.DTO(id: 0, name: "Mow lawn"), SwiftRex_ToDo_Persisted.ToDo.DTO(id: 1, name: "Wash car"), SwiftRex_ToDo_Persisted.ToDo.DTO(id: 2, name: "Clean windows"), SwiftRex_ToDo_Persisted.ToDo.DTO(id: 3, name: "Walk dog"), SwiftRex_ToDo_Persisted.ToDo.DTO(id: 4, name: "Cook dinner"), SwiftRex_ToDo_Persisted.ToDo.DTO(id: 5, name: "Pay bills")] Test Case '-[SwiftRex_ToDo_PersistedTests.SwiftRex_ToDo_PersistedTests testMiddlewarePublisherUsingExpectation]' passed (0.006 seconds). Test Suite 'SwiftRex_ToDo_PersistedTests' failed at 2020-10-04 13:23:17.815. Executed 3 tests, with 2 failures (0 unexpected) in 1.114 (1.116) seconds Test Suite 'SwiftRex-ToDo-PersistedTests.xctest' failed at 2020-10-04 13:23:17.816. Executed 3 tests, with 2 failures (0 unexpected) in 1.114 (1.116) seconds Test Suite 'All tests' failed at 2020-10-04 13:23:17.816. Executed 3 tests, with 2 failures (0 unexpected) in 1.114 (1.117) seconds

groue commented 3 years ago

Hello @rustproofFish,

Sorry I did not answer previously. Is this issue still relevant?

groue commented 3 years ago

Closing due to lack of activity