Проверка и проверка результатов в модульном тестировании RxSwift - PullRequest
1 голос
/ 19 января 2020

Я только начал изучать 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 Ответ

1 голос
/ 20 января 2020

Вот полный, компилируемый ответ (не включая импорт.)

  • Вы указываете 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) { }
}
...