【问题标题】:Mocking and Validating Results in RxSwift Unit Testing在 RxSwift 单元测试中模拟和验证结果
【发布时间】:2020-05-05 21:12:56
【问题描述】:

我刚刚开始学习 RxSwift 并尝试构建一个示例应用程序来练习这些概念。

我编写了一个 QuestionViewModel,它从 QuestionOps 类加载问题列表。 QuestionOps 有一个 getQuestions 函数,它返回 Single。

我面临的问题是,如何在测试 QuestionViewModel 时模拟 QuestionOps 类的行为。

public class QuestionsListViewModel {

    public var questionOps: QuestionOps!

    private let disposeBag = DisposeBag()
    private let items = BehaviorRelay<[QuestionItemViewModel]>(value: [])
    public let loadNextPage = PublishSubject<Void>()
    public var listItems: Driver<[QuestionItemViewModel]>
    public init() {
        listItems = items.asDriver(onErrorJustReturn: [])

        loadNextPage
            .flatMapFirst { self.questionOps.getQuestions() }
            .map { $0.map { QuestionItemViewModel($0) } }
            .bind(to: items)
            .disposed(by: disposeBag)
    }
}
public class QuestionOps {

    public func getQuestions() -> Single<[Question]> {

        return Single.create { event -> Disposable in

            event(.success([]))
            return Disposables.create()
        }
    }
}

我创建了这个 MockQuestionOps 用于测试目的:

public class MockQuestionOps : QuestionOps {

    //MARK: -
    //MARK: Responses
    public var getQuestionsResponse: Single<[Question]>?

    public func getQuestions() -> Single<[Question]> {
        self.getQuestionsResponse = Single.create { event -> Disposable in

            return Disposables.create()
        }
        return self.getQuestionsResponse!
    }
}

在我的测试用例中,我正在执行以下操作:

/// My idea here is to test in following maner:
/// - at some point user initates loading
/// - after some time got network response with status true
func testLoadedDataIsDisplayedCorrectly() {

    scheduler = TestScheduler(initialClock: 0)
    let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)

    let qOps = MockQuestionOps()
    vm = QuestionsListViewModel()
    vm.questionOps = qOps
    vm.listItems
        .drive(questionsLoadedObserver)
        .disposed(by: disposebag)

    // User initiates load questions
    scheduler.createColdObservable([.next(2, ())])
        .bind(to: vm.loadNextPage)
        .disposed(by: disposebag)

    // Simulating question ops behaviour of responding
    // to get question request

    /// HERE: -----------    
    /// This is where I am stuck
    /// How should I tell qOps to send particular response with delay

    scheduler.start()

    /// HERE: -----------
    /// How can I test list is initialy empty
    /// and after loading, data is correctly loaded
}

【问题讨论】:

    标签: ios rx-swift rxtest


    【解决方案1】:

    这是一个完整的、可编译的答案(不包括导入)。

    • 你告诉 qOps 发射,方法是给它一个冷测试 observable,它会发射正确的值。

    • 通过将测试观察者收集的事件与预期结果进行比较来测试输出。

    没有“列表最初为空”的概念。该列表始​​终为空。它会随着时间的推移发出值,而您要测试的是它是否发出了正确的值。

    class rx_sandboxTests: XCTestCase {
    
        func testLoadedDataIsDisplayedCorrectly() {
    
            let scheduler = TestScheduler(initialClock: 0)
            let disposebag = DisposeBag()
            let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
    
            let qOps = MockQuestionOps(scheduler: scheduler)
            let vm = QuestionsListViewModel(questionOps: qOps)
            vm.listItems
                .drive(questionsLoadedObserver)
                .disposed(by: disposebag)
    
            scheduler.createColdObservable([.next(2, ())])
                .bind(to: vm.loadNextPage)
                .disposed(by: disposebag)
    
            scheduler.start()
    
            XCTAssertEqual(questionsLoadedObserver.events, [.next(12, [QuestionItemViewModel(), QuestionItemViewModel()])])
        }
    }
    
    protocol QuestionOpsType {
        func getQuestions() -> Single<[Question]>
    }
    
    struct MockQuestionOps: QuestionOpsType {
        func getQuestions() -> Single<[Question]> {
            return scheduler.createColdObservable([.next(10, [Question(), Question()]), .completed(10)]).asSingle()
        }
        let scheduler: TestScheduler
    }
    
    class QuestionsListViewModel {
    
        let listItems: Driver<[QuestionItemViewModel]>
        private let _loadNextPage = PublishSubject<Void>()
    
        var loadNextPage: AnyObserver<Void> {
            return _loadNextPage.asObserver()
        }
    
        init(questionOps: QuestionOpsType) {
            listItems = _loadNextPage
                .flatMapFirst { [questionOps] in
                    questionOps.getQuestions().asObservable()
                }
    
                .map { $0.map { QuestionItemViewModel($0) } }
                .asDriver(onErrorJustReturn: [])
        }
    }
    
    struct Question { }
    struct QuestionItemViewModel: Equatable {
        init() { }
        init(_ question: Question) { }
    }
    

    【讨论】:

    • 很好的解释谢谢,不过有一个问题。上面你已经从 MockQuesitonOps 发送了空的 Question 对象。是否足够或者我们应该发送带有值的问题并在我们的测试用例中检查值是否正确?
    猜你喜欢
    • 1970-01-01
    • 2021-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多