如何在 RxTest 中测试多个事件

How to test multiple events in RxTest

提问人:Jiss 提问时间:6/27/2023 更新时间:6/27/2023 访问量:66

问:

我们目前正在为我们的 ViewModel 实现单元测试。

当 requestInAppPayment 输入请求来自视图模型时,仅当实例属性 isPurchasing 筛选器为 false 时,才会实现订阅(以防止多次触摸购买按钮)。

我想在购买过程中按下购买按钮时为过滤器编写一个测试用例,但我不知道该怎么做。

我想编写一个测试用例,用于在购买过程中单击购买按钮时进行筛选。

在 ViewMode 中

requestInAppPayment
            .filter { [weak self] _ in
                self?.isPurchasing == false
            }
            .subscribe(with: self, onNext: { owner, inAppPayment in
                owner.isPurchasing = true
                owner.showIndicator.onNext(())
                owner.requestInAppPurchaseUseCase.execute(productID: inAppPayment.productID)
            })
            .disposed(by: disposeBag)

 requestInAppPurchaseUseCase.purchaseSuccess
            .do(onNext: { [weak self] _ in
                self?.isPurchasing = false
                self?.hideIndicator.onNext(())
            })

在单元测试中

       let observer = scheduler.createObserver(String.self)
       let stubPaymentItem = InAppPayment()
        
        scheduler.createHotObservable([
            .next(10, stubPaymentItem),
            .next(20, stubPaymentItem)
        ])
        .bind(to: viewModel.input.requestInAppPurchase)
        .disposed(by: disposeBag)
        
        viewModel.output.showPaymentResultPage
            .map { "success" }
            .drive(observer)
            .disposed(by: disposeBag)
        
        scheduler.start()
        
// result = [.next(10, "success"), .next(20, "success")],
// The logic I think is [.next(10, "success")]

    XCTAssertEqual(observer.events, [
          .next(10, "success")
        ])
SWIFT5 夫特 Swift5 RX-SWIFT RXTEST

评论

0赞 Daniel T. 6/27/2023
嗨,@Jiss。缺少一些信息...视图模型中没有(似乎大部分视图模型完全丢失了。在实际应用中如何生成对象?showPaymentResultPageInAppPayment
0赞 Daniel T. 6/27/2023
如果确保在IAP进行时不执行IAP是你的目标,那么你就太努力了。您只需要使用以防止多次触摸购买按钮。flatMapFirst
0赞 Jiss 6/29/2023
嗨,@DanielT。首先,我没有太多信息,但感谢您展示示例代码。ViewMode 输出中的 purchaseSuccess 最初是 showPaymentResultPage。我不熟悉 RxSwift,所以我不知道 flatMapFirst Operator 的存在。该代码使用 flatMapFirst 正常处理测试。再次感谢您的回复..!

答:

0赞 Daniel T. 6/27/2023 #1

这是一个完全可行的示例,仅基于您提供的单元测试的少量内容。我根本没有使用你的视图模型代码,我只是写了一个通过测试的视图模型。

我使用了我在这里提供的测试工具:https://gist.github.com/danielt1263/bd449100764e3166644f7a38bca86c96

class ViewModelTests: XCTestCase {
    func test() {
        let scheduler = TestScheduler(initialClock: 0)
        let disposeBag = DisposeBag()
        let iapObserver = scheduler.createObserver(InAppPayment.self)
        let mockUseCase = MockRIAP(scheduler: scheduler)
        let viewModel = ViewModel(requestInAppPurchaseUseCase: mockUseCase)
        // I had to add all of the above to make the test compile.
        let observer = scheduler.createObserver(String.self)
        let stubPaymentItem = InAppPayment(id: 25) // I had to give the object an ID

        scheduler.createObservable(timeline: "-AA", values: ["A": stubPaymentItem]) // waits one second then emits two stubPaymentItems one second apart.
        .bind(to: viewModel.input.requestInAppPurchase)
        .disposed(by: disposeBag)

        viewModel.output.showPaymentResultPage
            .map { "success" }
            .drive(observer)
            .disposed(by: disposeBag)

        scheduler.start()

        XCTAssertEqual(observer.events, [
            .next(2, "success") // success emits at the 2 second mark because the trigger waits a second and then the mock waits a second. The second trigger is ignored.
        ])
    }
}

class MockRIAP: RequestInAppPurchaseUseCase {
    let args: TestableObserver<InAppPayment.ID>
    let _execute: (InAppPayment.ID) -> Observable<IAPResponse>

    init(scheduler: TestScheduler) {
        args = scheduler.createObserver(InAppPayment.ID.self)
        _execute = scheduler.mock(args: args, values: ["A": IAPResponse()], timelineSelector: { _ in "-A|" }) // waits one second then emits an IAPResponse
    }
    func execute(id: InAppPayment.ID) -> RxSwift.Observable<IAPResponse> {
        _execute(id)
    }
}

以下是使上述工作正常工作的生产代码:

protocol RequestInAppPurchaseUseCase {
    func execute(id: InAppPayment.ID) -> Observable<IAPResponse>
}

struct ViewModel {
    struct Input {
        let requestInAppPurchase: AnyObserver<InAppPayment>
    }
    struct Output {
        let showPaymentResultPage: Driver<Void> // a Driver<Void> doesn't make sense. Why would you want to emit the previous result? Better would be a Signal<Void>.
    }
    let input: Input
    let output: Output
}

extension ViewModel {
    init(requestInAppPurchaseUseCase: RequestInAppPurchaseUseCase) {
        let _requestInAppPurchase = PublishSubject<InAppPayment>()
        let showResults = _requestInAppPurchase
            .flatMapFirst { purchase in
                requestInAppPurchaseUseCase.execute(id: purchase.id)
            }
            .map { _ in }
            .asDriver(onErrorDriveWith: .empty())

        input = Input(requestInAppPurchase: _requestInAppPurchase.asObserver())
        output = Output(showPaymentResultPage: showResults)
    }
}

struct InAppPayment: Identifiable {
    let id: Int
}

struct IAPResponse { }