ios - 如何对 RxCocoa BehaviorRelay 进行单元测试

标签 ios xctest rx-swift rx-cocoa

我开始进行单元测试 RxSwift Driver。我在测试 Driver 时遇到问题。

这是我的 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)
            }
        }
    }
}

我的单元测试看起来像这样:

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)
    }
}

我的问题:

我正在尝试跟踪 loading 驱动程序变量的状态。但是,它始终为 false。即使在编写了一个用于检查状态的调试器之后,它也只打印出一个值,并且始终为 false

我决定在代码中添加一个断点,我注意到 让 loadingState = 尝试! loadingObservable.skip(0).toBlocking().first()! 只有在函数执行完毕后才会调用。

有没有办法测试 loading 状态? 是否需要测试 loading 状态?

谢谢。

最佳答案

我认为问题在于 RxBlocking 仅处理发出的第一个事件。您需要查看一系列事件。考虑改用 RxTest。这是一个使用 RxTest 的单元测试,它通过了您创建的 View 模型:

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

}

关于ios - 如何对 RxCocoa BehaviorRelay 进行单元测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54165009/

相关文章:

ios - ios UI更新代码如何定义用于主调度

error-handling - UI 测试失败,错误为 "Failed to get snapshot within 15.0s"

swift - Xcode 9 中的 Playground 是否使用 Swift 4.0 编译

ios - 点击单元格时 Xcode UI 测试 XCTDaemonErrorDomain Code=13

swift - 如何使用 RxSwift 订阅数组变化?

swift - 在 observable 中处理一次性用品的正确方法

ios - 导航栏标题仅更改一次(不再变白然后返回)

iphone - 适用于iOS的2D游戏引擎

ios - ios 7 Storyboard上的自定义标签栏

xcode - 如何获得通用应用程序的组合代码覆盖率?