Test PublishSubject for ViewState

906 Views Asked by At

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

1

There are 1 best solutions below

0
On BEST ANSWER

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