Search code examples
mvvmrx-swiftrxtestrx-blocking

Test PublishSubject for ViewState


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


Solution

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