Как выполнить модульное тестирование RxCocoa BehaviorRelay
Я начинаю с модульного тестирования 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)
}
}
}
}
И мой 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)
}
}
Мой вопрос:
Я пытаюсь отследить состояние loading
переменная драйвера. Но это всегда false
, Даже после написания отладчика для проверки состояний он выводит только одно значение и всегда false
,
Я решил добавить точку останова в код, и я заметил, let loadingState = try! loadingObservable.skip(0).toBlocking().first()!
вызывается только после завершения выполнения функции.
Есть ли способ проверить loading
государство? Нужно ли тестировать на loading
государство?
Благодарю.
1 ответ
Я считаю, что проблема в том, что RxBlocking имеет дело только с первым событием, которое отправляется. Вам нужно посмотреть на серию событий. Взгляните на использование RxTest вместо этого. Вот модульный тест с использованием 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
}