测试 ViewState 的 PublishSubject

Test PublishSubject for ViewState

我正在尝试测试我的 ViewModel 的主要功能。重要的一步是测试加载状态完成。但可以肯定的是,为了更好的测试,测试所有状态可能会很有趣。

我阅读了很多 post 和有关 RxTest 和 RxBlocking 的信息,但我无法测试此模块。如果有人能帮助我,那就太好了!

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

目标是能够在调用 viewmodel.requestData() 后测试所有 ProductsViewState

这里的关键是你必须将你的调度程序注入到函数中,这样你才能注入一个测试调度程序。然后你就可以测试你的state了。顺便说一句 state 属性 应该是 let 而不是 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()
    }
}