简体   繁体   English

如何对RxCocoa BehaviorRelay进行单元测试

[英]How to unit test RxCocoa BehaviorRelay

I am starting out with unit testing RxSwift Driver . 我从单元测试RxSwift Driver And I am having issues testing a Driver . 而且我在测试Driver时遇到问题。

This is the code structure of my ViewModel : 这是我的ViewModel的代码结构:

import Foundation
import RxSwift
import RxCocoa

class LoginViewViewModel {

    private let loginService: LoginService

    private let _loading = BehaviorRelay<Bool>(value: false)
    private let _loginResponse = BehaviorRelay<LoginResponse?>(value: nil)
    private let _phoneMessage = BehaviorRelay<String>(value: "")
    private let _pinMessage = BehaviorRelay<String>(value: "")
    private let _enableButton = BehaviorRelay<Bool>(value: false)

    var loginResponse: Driver<LoginResponse?> { return _loginResponse.asDriver() }
    var loading: Driver<Bool> { return _loading.asDriver() }
    var phoneMessage: Driver<String> { return _phoneMessage.asDriver() }
    var pinMessage: Driver<String> { return _pinMessage.asDriver() }
    var enableButton: Driver<Bool> { return _enableButton.asDriver() }

    private let phone = BehaviorRelay<String>(value: "")
    private let pin = BehaviorRelay<String>(value: "")

    private let disposeBag = DisposeBag()

    init(phone: Driver<String>, pin: Driver<String>, buttonTapped: Driver<Void>, loginService: LoginService) {
        self.loginService = loginService
        phone
            .throttle(0.5)
            .distinctUntilChanged()
            .drive(onNext: { [weak self] (phone) in
                self?.phone.accept(phone)
                self?.validateFields()
            }).disposed(by: disposeBag)

        pin
            .throttle(0.5)
            .distinctUntilChanged()
            .drive(onNext: { [weak self] (pin) in
                self?.pin.accept(pin)
                self?.validateFields()
            }).disposed(by: disposeBag)

        buttonTapped
            .drive(onNext: { [weak self] () in
                self?.loginUser(phone: self!.phone.value, pin: self!.pin.value)
            }).disposed(by: disposeBag)
    }

    private func validateFields() {
        guard phone.value.count > 0 else {
            return
        }
        _enableButton.accept(false)

        guard pin.value.count > 0 else {
            return
        }

        _enableButton.accept(true)
        _phoneMessage.accept("")
        _pinMessage.accept("")
    }

    private func loginUser(phone: String, pin: String) {
        _loading.accept(true)
        _phoneMessage.accept("")
        _pinMessage.accept("")

        loginService.loginUser(phone: phone, pin: pin) { [weak self] (response, error) in
            self?._loading.accept(false)
            if let error = error {
                if error.message! == "Invalid credentials" {
                    self?._phoneMessage.accept("Invalid Phone Number")
                    self?._pinMessage.accept("Invalid Pin Provided")
                }
            } else {
                response?.saveUserInfo()
                self?._loginResponse.accept(response)
            }
        }
    }
}

And my UnitTest looks like this: 我的UnitTest看起来像这样:

class LoginViewViewModelTest: XCTestCase {

    private class MockLoginService: LoginService {
        func loginUser(phone: String, pin: String, completion: @escaping LoginService.LoginDataCompletion) {
            guard phone == "+17045674568", pin == "1234" else {
                let loginresponse = LoginResponse(message: "Login Successfully", status: true, status_code: 200, data: LoginData(access_token: "adadksdewffjfwe", token_type: "bearer", expires_in: 3600, expiry_time: "today", user: User(id: "1dsldsdsjkj", name: "RandomGuy", phone: "12345", pin_set: true, custom_email: false, email: "somerandom@email.com")))
                completion(loginresponse, nil)
                return
            }
            let akuError = AKUError(status: false, message: "Invalid Credential.", status_code: "404")
            completion(nil, akuError)
        }
    }

    var viewModel: LoginViewViewModel!

    var scheduler: SchedulerType!

    var phone: BehaviorRelay<String>!
    var pin: BehaviorRelay<String>!
    var buttonClicked: BehaviorRelay<Void>!

    override func setUp() {
        super.setUp()

        phone = BehaviorRelay<String>(value: "")
        pin = BehaviorRelay<String>(value: "")
        buttonClicked = BehaviorRelay<Void>(value: ())

        let loginService = MockLoginService()

        viewModel = LoginViewViewModel(phone: phone.asDriver(), pin: pin.asDriver(), buttonTapped: buttonClicked.asDriver(), loginService: loginService)

        scheduler = ConcurrentDispatchQueueScheduler(qos: .default)
    }

    override func tearDown() {
        super.tearDown()
    }

    func testLoginButtonClicked_Loading() {
        let loadingObservable = viewModel.loading.asObservable().subscribeOn(scheduler)

        phone.accept("12345")
        pin.accept("12345")
        buttonClicked.accept(())

        let loadingState = try! loadingObservable.skip(0).toBlocking().first()!
        XCTAssertNotNil(loadingState)
        XCTAssertEqual(loadingState, true)
    }
}

My question: 我的问题:

I am trying to track the state of the loading driver variable. 我正在尝试跟踪loading驱动程序变量的状态。 But, it's always false . 但是,它总是false Even after writing a debugger for checking the states, it only prints out one value and, it's always false . 即使在编写调试器以检查状态之后,它也只打印出一个值,并且始终为false

I decided to add a break point to the code, and I noticed let loadingState = try! loadingObservable.skip(0).toBlocking().first()! 我决定在代码中添加一个断点,并发现let loadingState = try! loadingObservable.skip(0).toBlocking().first()! let loadingState = try! loadingObservable.skip(0).toBlocking().first()! only gets called once the function is done executing. 仅在函数完成执行后才被调用。

Is there a way to test for the loading state? 有没有一种方法可以测试loading状态? Is it necessary to test for the loading state? 是否需要测试loading状态?

Thanks. 谢谢。

I believe the problem is that RxBlocking only deals with the first event that is emitted. 我认为问题在于RxBlocking仅处理发出的第一个事件。 You need to look at a series of events. 您需要查看一系列事件。 Look into using RxTest instead. 研究使用RxTest代替。 Here is a unit test using RxTest that passes with the view model you created: 这是使用RxTest的单元测试,该测试与您创建的视图模型一起传递:

class LoginLoadingTests: XCTestCase {

    var scheduler: TestScheduler!
    var result: TestableObserver<Bool>!
    var bag: DisposeBag!

    override func setUp() {
        super.setUp()
        scheduler = TestScheduler(initialClock: 0)
        result = scheduler.createObserver(Bool.self)
        bag = DisposeBag()
    }

    func testLoading() {
        let loginService = MockLoginService { phone, pin, response in
            self.scheduler.scheduleAt(20, action: { response(nil, RxError.unknown) })
        }

        let tap = scheduler.createHotObservable([.next(10, ())])
        let viewModel = LoginViewViewModel(phone: Driver.just("9876543210"), pin: Driver.just("1234"), buttonTapped: tap.asDriver(onErrorJustReturn: ()), loginService: loginService)

        viewModel.loading
            .drive(result)
            .disposed(by: bag)

        scheduler.start()

        XCTAssertEqual(result.events, [
            .next(0, false),
            .next(10, true),
            .next(20, false)
            ])
    }
}

struct MockLoginService: LoginService {
    init(loginUser: @escaping (_ phone: String, _ pin: String, _ response: @escaping (LoginResponse?, Error?) -> Void) -> Void) {
        _loginUser = loginUser
    }
    func loginUser(phone: String, pin: String, response: @escaping (LoginResponse?, Error?) -> ()) {
        _loginUser(phone, pin, response)
    }

    let _loginUser: (_ phone: String, _ pin: String, _ response: @escaping (LoginResponse?, Error?) -> Void) -> Void

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM