Lifting Into a New Type: My first "Idiomatic" RxSwift Unit Test

I dabble with RxSwift right now. Figuring out how to write tests for pure functions and reactive code, I tried to write an assertion for incoming events:

let expected: [Recorded<Event<URL?>>] = [next(0, nil), next(0, url)]
XCTAssertEqual(observer.events, expected)

Doesn’t compile because equating arrays with Optional<URL> in them won’t. In other words, [Recorded<Event<URL>>] (non-optional) would work.

I tries to figure out how to make this equatable, but then I stopped – why not lift the URL? into its own type? After all, the signal I’m observing is a path settings change. nil is allowed to signify removing the setting, like when you reset to defaults.

Lifting this into its own “either-or” type, also known as encapsulating the information in an enum, I end up with this:

enum URLChange {
    case set(URL)
    case unset
}

Making this equatable is simple. And the tests work, too. For the record, here’s the idiomatic Rx test:

func testArchiveURLChanges_RxVersion() {

    let adapter = SettingsAdapter(defaults: UserDefaults.standard)
    let url = URL(fileURLWithPath: "/a/url/", isDirectory: true)
    
    let disposeBag = DisposeBag()
    let scheduler = TestScheduler(initialClock: 0)
    let observer = scheduler.createObserver(URLChange.self)

    adapter.archiveURLChange()
        .subscribe(observer)
        .addDisposableTo(disposeBag)

    defaults.set(url, forKey: "archiveUrl")

    let expected: [Recorded<Event<URLChange>>] = [
        next(0, .unset),   // Initial value for fresh defaults
        next(0, .set(url))
    ]
    XCTAssertEqual(observer.events, expected)
}

And the traditional, asynchronous version I used before:

func testArchiveURLChanges() {

    let adapter = SettingsAdapter(defaults: UserDefaults.standard)
    let ex = expectation(description: "Reports back result")
    let url = URL(fileURLWithPath: "/a/url/", isDirectory: true)
    let disposeBag = DisposeBag()
    
    adapter.archiveURLChange()
        .subscribe(onNext: { if $0.url == url { ex.fulfill() } })
        .addDisposableTo(disposeBag)

    defaults.set(url, forKey: "archiveUrl")

    waitForExpectations(timeout: 0.2, handler: nil)
}

I’m not too happy with the verbose setup of the Rx test case. But I’m refactoring the setup into – wait for it – the XCTestCase.setUp() method.

For simple assertions, the async variant would work just as well. Recording longer sequences of events with some timing is interesting, though I didn’t need that, yet, to test-drive my operators.

Browse the blog archive