ios – TestScheduler recorded event is off by 1


I’ve got this classes that I would like to test. It is for polling a state from the API until it is successfully activated.

struct Entity {
    let id: String
    let name: String
    let status: EntityStatus
}

extension Entity {
    static func sample(id: String = "sample-id-123456",
                       name: String = "Gorgeous Me",
                       status: EntityStatus = .allCases.randomElement()!) -> Entity {
        return Entity(id: id, name: name, status: status)
    }
}

enum EntityStatus: Equatable, CaseIterable {
    case unknown
    case waiting
    case active
}

protocol EntityRepositoryProtocol {
    func getEntity(for id: String) -> Observable<Entity>
}

final class EntityRepository: EntityRepositoryProtocol {
    private let api: ApiClientProtocol
    init(let api: ApiClientProtocol) {
        self.api = api
    }
    
    func getEntity(for id: String) -> Observable<Entity> {
        return api.get(endpoint: "entity", params: "id=\(id)")
            .map { EntityResponseModel.mapToEntity($0) }
    }
}

final class MockEntityRepository: EntityRepositoryProtocol {
    private let getEntityObservable: Observable<Entity>
    init(getEntityObservable: Observable<Entity> = .empty()) {
        self.getEntityObservable = getEntityObservable
    }
    
    func getEntity(for id: String) -> Observable<Entity> {
        return getEntityObservable
    }
}

typealias ResultEntity = Result<Entity, Error>

protocol EntityPollerProtocol {
    func poll(id: String,
              per intervalMillis: Int,
              for timeoutSeconds: Int) -> Observable<ResultEntity>
}

final class EntityPoller: EntityPollerProtocol {
    private let repository: EntityRepositoryProtocol
    private let scheduler: SchedulerType
    init(repository: EntityRepositoryProtocol,
         scheduler: SchedulerType) {
        self.repository = repository
        self.scheduler = scheduler
    }

    func poll(id: String,
              per intervalMillis: Int,
              for timeoutSeconds: Int) -> Observable<ResultEntity> {
        let getEntity = Observable<Int>
            .interval(.milliseconds(intervalMillis), scheduler: scheduler)
            .startWith(0)
            .flatMapLatest { [weak self] _ -> Observable<Entity> in
                guard let self = self else {
                    return .never()
                }
                return self.repository.getEntity(for: id)
            }

            
        let timeout = Observable.just(Entity(id: "", name: "", status: .unknown))
            .delay(.seconds(timeoutSeconds), scheduler: scheduler)
            .map { _ -> Entity in throw ApiError.dataNotFound }
        
        return Observable.merge(getEntity, timeout)
            .filter { entity in entity.status != .waiting }
            .map { entity in
                switch entity.status {
                case .active: return ResultEntity.success(entity)
                default: return ResultEntity.failure(NSError())
                }
            }
            .catch { error in
                return .just(ResultEntity.failure(error))
            }
            .take(1)
    }
}

I have this test that should have worked but the observed event is the one fired next. Not the one I thought will be fired.

func testPoll_ShouldOnlyReturnTrueOnce_WhenStatusActive() throws {
    let sampleId = "test-poll-id-123456"
    let scheduler = TestScheduler(initialClock: 0, resolution: 0.001)
    let sutObserver = scheduler.createObserver(Bool.self)
    
    let entityOutputs = scheduler.createColdObservable([
        .next(100, Entity.sample(id: sampleId, status: .waiting)),
        .next(200, Entity.sample(id: sampleId, status: .waiting)),
        .next(300, Entity.sample(id: sampleId, status: .active)),
        .next(400, Entity.sample(id: sampleId, status: .active)),
        .next(500, Entity.sample(id: sampleId, status: .active))
    ]).asObservable()
    
    let bindingInfoRepository = MockEntityRepository(getEntityObservable:entityOutputs)
    
    let sut = EntityPoller(repository: EntityRepositoryProtocol,
                           scheduler: scheduler)
    
    sut.poll(id: sampleId, per: 100, for: 180000)
        .bind(to: sutObserver)
        .disposed(by: disposeBag)
    
    scheduler.advanceTo(600)
    
    XCTAssertEqual([.next(300, Entity.sample(id: sampleId, status: .active))), .completed(300)],
                   sutObserver.events)
}

This should work, however, the event that is fired is the one on the 400 millisecs. So it is off by 1. And it threw this error:

testPoll_ShouldOnlyReturnTrueOnce_WhenStatusActive(): XCTAssertEqual
failed: (“[next(Entity…) @ 300, completed @ 300]”) is not equal to
(“[next(Entity…) @ 400, completed @ 400]”)

Why does this happen? Thanks.

Latest articles

spot_imgspot_img

Related articles

Leave a reply

Please enter your comment!
Please enter your name here

spot_imgspot_img