ReactiveX / RxSwift

Reactive Programming in Swift
MIT License
24.38k stars 4.17k forks source link

RxTest didn't work with Swift concurrency #2539

Open tomcheung opened 1 year ago

tomcheung commented 1 year ago

Short description of the issue:

RxTest TestScheduler didn't work if the observable stream include async await usage (wrapped with AsyncThrowingStream) In some case it will throw Fatal error: Executing on background thread. Please use MainScheduler.instance.schedule to schedule work on main thread

No matter using @MainActor or not, it won't get any value when checking the output from TestableObserver

Expected outcome:

Test case run successfully

What actually happens:

Test case got empty event

Self contained code example that reproduces the issue:

class MyClass {
    @MainActor func doubleValue(input: Observable<Int>) -> Observable<Int> { // Remove @MainActo will trigger error
        return AsyncThrowingStream {
            let val = try await input.first().value ?? 0
            return val * 2 // Use dummy value for testing here, however sometime may consume another async function in here
        }.asObservable()
    }
}

@MainActor final class RxSwiftTestingTests: XCTestCase {
    var disposeBag = DisposeBag()

    func testMyClass() throws {
        let scheduler = TestScheduler(initialClock: 0)

        let input = scheduler.createColdObservable([
            .next(0, 1),
            .completed(0)
        ]).asObservable()

        let c = MyClass()

        let observer = scheduler.createObserver(Int.self)
        c.doubleValue(input: input).bind(to: observer).disposed(by: disposeBag)

        scheduler.start()

        XCTAssertEqual(observer.events, [
            .next(0, 2),
            .completed(0)
        ])
    }
}

RxSwift/RxCocoa/RxBlocking/RxTest version/commit

RxSwift 6.6.0

Platform/Environment

How easy is to reproduce? (chances of successful reproduce after running the self contained code)

Xcode version:

  14.3.1 (14E300c)
danielt1263 commented 1 year ago

This is expected behavior. A TestScheduler is not asynchronous. If you need to test asynchronous behavior, you will have to follow the instructions in Asynchronous Tests and Expectations instead of using TestScheduler.

This is the reason (or at least part of the reason) the async producer operators (eg timer) all accept a Scheduler as a parameter, so you can substitute a TestScheduler and turn them synchronous.

nikolaykasyanov commented 1 year ago

I wonder if the introduction of custom executors in Swift 5.9 will make it possible to force concurrent Swift code to use TestScheduler.