3

A ViewModel has an input (an observer) which is bound to tap event of UIButton in UIViewController. This observer is of type AnyObserver<Void>.

In my unit test, this is what I'm expecting:

let correctValues: [Recorded<Event<Void>>] = Recorded.events(
       .next(0, ()),
       .completed(0)
   )

My test observer definition is:

private var voidEventsObserver: TestableObserver<Void>!

let scheduler = TestScheduler(initialClock: 0)
voidEventsObserver = scheduler.createObserver(Void.self)

Assert statement:

XCTAssertEqual(voidEventsObserver.events, correctValues)

I'm getting following error:

Expression type '()' is ambiguous without more context

In Rx, Void events are normal and to properly test ViewModel, one needs to compare them. e.g. .next(0, ()), .completed(0) etc. Void is not Equatable and it doesn't make sense to make it Equatable. However, I need to assert if the event is .next or .error or .completed. How do I assert that part?

HitMan
  • 195
  • 1
  • 9

2 Answers2

4

Working with Void can be a pain at times.

Played around with your example, but adding some conditional conformance to Equatable for Result or Event that contain Void is not possible due to Void not being a nominal type or due to these types already having a conflicting conformance to Equatable.

One approach would be to do something like this:

XCTAssertEqual(voidEventsObserver.events.count, correctValues.count)

for (actual, expected) in zip(voidEventsObserver.events, correctValues) {
    XCTAssertEqual(actual.time, expected.time, "different times")

    let equal: Bool
    switch (actual.value, expected.value) {
    case (.next, .next),
         (.completed, .completed):
        equal = true
    default:
        equal = false
    }
    XCTAssertTrue(equal, "different event")
}

Now that's ugly as hell and hard to read. The other approach is to introduce a wrapper:

struct VoidRecord: Equatable {
    let record: Recorded<Event<Void>>

    static func == (lhs: Self, rhs: Self) -> Bool {
        guard lhs.record.time == rhs.record.time else { return false }

        switch (lhs.record.value, rhs.record.value) {
        case (.next, .next),
             (.completed, .completed):
            return true
        default:
            return false
        }
    }
}

XCTAssertEqual(
    voidEventsObserver.events.map(VoidRecord.init),
    correctValues.map(VoidRecord.init)
)

That reads a lot nicer. Note that the above treats .error events as always being different. If you need to compare error events, simply add this logic from RxSwift to the == function above.

fphilipe
  • 9,739
  • 1
  • 40
  • 52
  • Shouldn't this be a part of RxTest? What's your say? – HitMan Dec 24 '19 at 12:36
  • 2
    Found [this PR](https://github.com/ReactiveX/RxSwift/pull/1747) that wasn't merged. Maintainer argues that this should be fixed in the language. Interesting solution mentioned in the PR is to simply map the observable to `Observable` so you can compare. – fphilipe Dec 24 '19 at 16:06
0

This is the solution that has worked for me. It replaces Void with equatable VoidValue for Event. Later it uses XCTAssertEqual which is part of RxSwift library so the behavior and output will be consistent.

func XCTAssertEqual(_ lhs: [Recorded<Event<Void>>], _ rhs: [Recorded<Event<Void>>], file: StaticString = #file, line: UInt = #line) {
    
    struct VoidValue: Equatable {
        static func ==(lhs: Self, rhs: Self) -> Bool {
            return true
        }
    }
    
    let toVoidValueEvent: (Event<Void>) -> Event<VoidValue> = { event in
        switch event {
        case .completed:
            return .completed
        case .error(let e1)
            :
            return .error(e1)
        case .next:
            return .next(.init())
        }
    }
    
    let lhsRecords: [Recorded<Event<VoidValue>>] = lhs.map({ Recorded(time: $0.time, value: toVoidValueEvent($0.value)) })
    let rhsRecords: [Recorded<Event<VoidValue>>] = rhs.map({ Recorded(time: $0.time, value: toVoidValueEvent($0.value)) })
    
    XCTAssertEqual(lhsRecords, rhsRecords)
}