I'm trying to test the main functionality of my ViewModel. The important step is to test te loaded state completed. But for sure, for a better test it could be interesting to test al states.
I was reading a lot of post and information about RxTest and RxBlocking but I'm not able to test this module. If someone can help me, it would be great!
struct Product: Equatable { }
struct Promotion { }
protocol ProductsRepository {
func fetchProducts() -> Observable<Products>
func fetchPromotions() -> Observable<[Promotion]>
}
struct ProductCellViewModel: Equatable {
let product: Product
}
struct Products {
let products: [Product]
}
enum ProductsViewState: Equatable {
case loading
case empty
case error
case loaded ([ProductCellViewModel])
}
class ProductsViewModel {
var repository: ProductsRepository
let disposeBag = DisposeBag()
private var productCellViewModel: [ProductCellViewModel]
private var promotions: [Promotion]
// MARK: Input
init(repository: ProductsRepository) {
self.repository = repository
productCellViewModel = [ProductCellViewModel]()
promotions = [Promotion]()
}
func requestData(scheduler: SchedulerType) {
state.onNext(.loading)
resetCalculate()
repository.fetchProducts()
.observeOn(scheduler)
.flatMap({ (products) -> Observable<[ProductCellViewModel]> in
return self.buildCellViewModels(data: products)
}).subscribe(onNext: { (cellViewModels) in
self.productCellViewModel = cellViewModels
}, onError: { (error) in
self.state.onNext(.error)
}, onCompleted: {
self.repository.fetchPromotions()
.flatMap({ (promotions) -> Observable<[Promotion]> in
self.promotions = promotions
return Observable.just(promotions)
}).subscribe(onNext: { (_) in
self.state.onNext(.loaded(self.productCellViewModel))
}, onError: { (error) in
self.state.onNext(.error)
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
}
// MARK: Output
var state = PublishSubject<ProductsViewState>()
// MARK: ViewModel Map Methods
private func buildCellViewModels(data: Products) -> Observable <[ProductCellViewModel]> {
var viewModels = [ProductCellViewModel]()
for product in data.products {
viewModels.append(ProductCellViewModel.init(product: product))
}
return Observable.just(viewModels)
}
func resetCalculate() {
productCellViewModel = [ProductCellViewModel]()
}
}
The goal is to be able to test all of ProductsViewState after viewmodel.requestData() is being called
The key here is that you have to inject your scheduler into the function so you can inject a test scheduler. Then you will be able to test your state
. BTW that state
property should be a let not a var.
class ProductsViewModelTests: XCTestCase {
var scheduler: TestScheduler!
var result: TestableObserver<ProductsViewState>!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
result = scheduler.createObserver(ProductsViewState.self)
disposeBag = DisposeBag()
}
func testStateLoaded() {
let mockRepo = MockProductsRepository(products: { .empty() }, promotions: { .empty() })
let viewModel = ProductsViewModel(repository: mockRepo)
viewModel.state.bind(to: result).disposed(by: disposeBag)
viewModel.requestData(scheduler: scheduler)
scheduler.start()
XCTAssertEqual(result.events, [.next(0, ProductsViewState.loading), .next(1, .loaded([]))])
}
func testState_ProductsError() {
let mockRepo = MockProductsRepository(products: { .error(StubError()) }, promotions: { .empty() })
let viewModel = ProductsViewModel(repository: mockRepo)
viewModel.state.bind(to: result).disposed(by: disposeBag)
viewModel.requestData(scheduler: scheduler)
scheduler.start()
XCTAssertEqual(result.events, [.next(0, ProductsViewState.loading), .next(1, .error)])
}
func testState_PromotionsError() {
let mockRepo = MockProductsRepository(products: { .empty() }, promotions: { .error(StubError()) })
let viewModel = ProductsViewModel(repository: mockRepo)
viewModel.state.bind(to: result).disposed(by: disposeBag)
viewModel.requestData(scheduler: scheduler)
scheduler.start()
XCTAssertEqual(result.events, [.next(0, ProductsViewState.loading), .next(1, .error)])
}
}
struct StubError: Error { }
struct MockProductsRepository: ProductsRepository {
let products: () -> Observable<Products>
let promotions: () -> Observable<[Promotion]>
func fetchProducts() -> Observable<Products> {
return products()
}
func fetchPromotions() -> Observable<[Promotion]> {
return promotions()
}
}