Мокинг и проверка результатов в модульном тестировании RxSwift
Я только начал изучать RxSwift и пытался создать образец приложения, чтобы практиковать эти концепции.
Я написал QuestionViewModel, который загружает список вопросов из класса QuestionOps. В QuestionOps есть функция getQuestions, которая возвращает Single<[Question]>.
Проблема, с которой я столкнулся, заключается в том, как имитировать поведение класса QuestionOps при тестировании QuestionViewModel.
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
}
1 ответ
Вот полный, компилируемый ответ (не считая импорта).
Вы сообщаете qOps, что нужно испускать, давая ему наблюдаемый холодный тест, который выдаст правильные значения.
Вы тестируете выходные данные, сравнивая события, собранные наблюдателем тестирования, с ожидаемыми результатами.
Понятия "изначально список пуст" отсутствует. Список всегда пуст. Он генерирует значения с течением времени, и вы проверяете, выдал ли он правильные значения.
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) { }
}