在 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) { }
}
我刚刚开始学习 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) { }
}