在 RxSwift 单元测试中模拟和验证结果

Mocking and Validating Results in RxSwift Unit Testing

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

我编写了一个 QuestionViewModel,它从 QuestionOps class 加载问题列表。 QuestionOps 有一个函数 getQuestions returns Single<[Question]>.

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

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 { [=10=].map { QuestionItemViewModel([=10=]) } }
            .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
}

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

  • 你告诉 qOps 通过给它一个冷测试 observable 来发出,它会发出正确的值。

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

没有"the list is initially empty"的概念。该列表始​​终为空。它会随着时间的推移发出值,您要测试的是它是否发出正确的值。

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 { [=10=].map { QuestionItemViewModel([=10=]) } }
            .asDriver(onErrorJustReturn: [])
    }
}

struct Question { }
struct QuestionItemViewModel: Equatable {
    init() { }
    init(_ question: Question) { }
}