diff --git a/Tests/ReactiveKitTests/Helpers.swift b/Tests/ReactiveKitTests/Helpers.swift index c913cc5..12f86bc 100644 --- a/Tests/ReactiveKitTests/Helpers.swift +++ b/Tests/ReactiveKitTests/Helpers.swift @@ -39,21 +39,54 @@ extension Event { } extension SignalProtocol { - - func expectNext(_ expectedElements: [Element], - _ message: @autoclosure () -> String = "", - expectation: XCTestExpectation? = nil, - file: StaticString = #file, line: UInt = #line) { - expect(expectedElements.map { .next($0) } + [.completed], message, expectation: expectation, file: file, line: line) + + // Synchronous test + func expectComplete(after expectedElements: [Element], + file: StaticString = #file, line: UInt = #line) { + expect(events: expectedElements.map { .next($0) } + [.completed], file: file, line: line) } - func expect(_ expectedEvents: [Event], - _ message: @autoclosure () -> String = "", - expectation: XCTestExpectation? = nil, + func expect(events expectedEvents: [Event], file: StaticString = #file, line: UInt = #line) { var eventsToProcess = expectedEvents var receivedEvents: [Event] = [] - let message = message() + var matchedAll = false + let _ = observe { event in + receivedEvents.append(event) + if eventsToProcess.count == 0 { + XCTFail("Got more events than expected.") + return + } + let expected = eventsToProcess.removeFirst() + XCTAssert(event.isEqualTo(expected), "(Got \(receivedEvents) instead of \(expectedEvents))", file: file, line: line) + if eventsToProcess.count == 0 { + matchedAll = true + } + } + if !matchedAll { + XCTFail("Got only first \(receivedEvents.count) events of expected \(expectedEvents))", file: file, line: line) + } + } + + func expectNoEvent(file: StaticString = #file, line: UInt = #line) { + let _ = observe { event in + XCTFail("Got a \(event) when expected empty", file: file, line: line) + } + } + + // Asynchronous test + func expectAsyncComplete(after expectedElements: [Element], + expectation: XCTestExpectation, + file: StaticString = #file, line: UInt = #line) { + expectAsync(events: expectedElements.map { .next($0) } + [.completed], expectation: expectation, file: file, line: line) + } + + func expectAsync(events expectedEvents: [Event], + expectation: XCTestExpectation, + file: StaticString = #file, line: UInt = #line) { + XCTAssert(!expectedEvents.isEmpty, "Use expectEmptyAsync for waiting empty signal") + var eventsToProcess = expectedEvents + var receivedEvents: [Event] = [] let _ = observe { event in receivedEvents.append(event) if eventsToProcess.count == 0 { @@ -61,9 +94,9 @@ extension SignalProtocol { return } let expected = eventsToProcess.removeFirst() - XCTAssert(event.isEqualTo(expected), message + "(Got \(receivedEvents) instead of \(expectedEvents))", file: file, line: line) + XCTAssert(event.isEqualTo(expected), "(Got \(receivedEvents) instead of \(expectedEvents))", file: file, line: line) if eventsToProcess.count == 0 { - expectation?.fulfill() + expectation.fulfill() } } } diff --git a/Tests/ReactiveKitTests/PropertyTests.swift b/Tests/ReactiveKitTests/PropertyTests.swift index c4185d8..e88b70e 100644 --- a/Tests/ReactiveKitTests/PropertyTests.swift +++ b/Tests/ReactiveKitTests/PropertyTests.swift @@ -24,7 +24,7 @@ class PropertyTests: XCTestCase { } func testEvents() { - property.expect( + property.expectAsync(events: [ .next(0), .next(5), @@ -52,7 +52,7 @@ class PropertyTests: XCTestCase { var readOnlyView: AnyProperty! = property.readOnlyView XCTAssert(readOnlyView.value == 0) - readOnlyView.expect( + readOnlyView.expectAsync(events: [ .next(0), .next(5), @@ -84,11 +84,13 @@ class PropertyTests: XCTestCase { func testBidirectionalBind() { let target = Property(100) - target.expectNext([100, 0, 50, 60]) - property.expectNext([0, 0, 50, 60]) + target.ignoreTerminal().expectAsync(events: [.next(100), .next(0), .next(50), .next(60)], expectation: expectation(description: "nexts")) + property.ignoreTerminal().expectAsync(events: [.next(0), .next(0), .next(50), .next(60)], expectation: expectation(description: "nexts")) property.bidirectionalBind(to: target) property.value = 50 target.value = 60 + + waitForExpectations(timeout: 2, handler: nil) } } diff --git a/Tests/ReactiveKitTests/SignalTests.swift b/Tests/ReactiveKitTests/SignalTests.swift index c099750..81d5054 100644 --- a/Tests/ReactiveKitTests/SignalTests.swift +++ b/Tests/ReactiveKitTests/SignalTests.swift @@ -34,8 +34,8 @@ class SignalTests: XCTestCase { let operation = Signal.sequence([1, 2, 3]).executeIn(bob.context) - operation.expectNext([1, 2, 3]) - operation.expectNext([1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) XCTAssertEqual(bob.numberOfRuns, 2) } @@ -52,27 +52,27 @@ class SignalTests: XCTestCase { func testJust() { let operation = Signal.just(1) - operation.expectNext([1]) + operation.expectComplete(after: [1]) } func testSequence() { let operation = Signal.sequence([1, 2, 3]) - operation.expectNext([1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) } func testCompleted() { let operation = Signal.completed() - operation.expectNext([]) + operation.expectComplete(after: []) } func testNever() { let operation = Signal.never() - operation.expectNext([]) + operation.expectNoEvent() } func testFailed() { let operation = Signal.failed(.Error) - operation.expect([.failed(.Error)]) + operation.expect(events: [.failed(.Error)]) } func testObserveFailed() { @@ -96,99 +96,99 @@ class SignalTests: XCTestCase { func testBuffer() { let operation = Signal.sequence([1,2,3,4,5]) let buffered = operation.buffer(size: 2) - buffered.expectNext([[1, 2], [3, 4]]) + buffered.expectComplete(after: [[1, 2], [3, 4]]) } func testMap() { let operation = Signal.sequence([1, 2, 3]) let mapped = operation.map { $0 * 2 } - mapped.expectNext([2, 4, 6]) + mapped.expectComplete(after: [2, 4, 6]) } func testScan() { let operation = Signal.sequence([1, 2, 3]) let scanned = operation.scan(0, +) - scanned.expectNext([0, 1, 3, 6]) + scanned.expectComplete(after: [0, 1, 3, 6]) } func testToSignal() { let operation = Signal.sequence([1, 2, 3]) let operation2 = operation.toSignal() - operation2.expectNext([1, 2, 3]) + operation2.expectComplete(after: [1, 2, 3]) } func testSuppressError() { let operation = Signal.sequence([1, 2, 3]) let signal = operation.suppressError(logging: false) - signal.expectNext([1, 2, 3]) + signal.expectComplete(after: [1, 2, 3]) } func testSuppressError2() { let operation = Signal.failed(.Error) let signal = operation.suppressError(logging: false) - signal.expectNext([]) + signal.expectComplete(after: []) } func testRecover() { let operation = Signal.failed(.Error) let signal = operation.recover(with: 1) - signal.expectNext([1]) + signal.expectComplete(after: [1]) } func testWindow() { let operation = Signal.sequence([1, 2, 3]) let window = operation.window(size: 2) - window.merge().expectNext([1, 2]) + window.merge().expectComplete(after: [1, 2]) } // func testDebounce() { // let operation = Signal.interval(0.1, queue: Queue.global).take(first: 3) // let distinct = operation.debounce(interval: 0.3, on: Queue.global) // let exp = expectation(withDescription: "completed") - // distinct.expectNext([2], expectation: exp) + // distinct.expectComplete(after: [2], expectation: exp) // waitForExpectations(withTimeout: 1, handler: nil) // } func testDistinct() { let operation = Signal.sequence([1, 2, 2, 3]) let distinct = operation.distinct { a, b in a != b } - distinct.expectNext([1, 2, 3]) + distinct.expectComplete(after: [1, 2, 3]) } func testDistinct2() { let operation = Signal.sequence([1, 2, 2, 3]) let distinct = operation.distinct() - distinct.expectNext([1, 2, 3]) + distinct.expectComplete(after: [1, 2, 3]) } func testElementAt() { let operation = Signal.sequence([1, 2, 3]) let elementAt1 = operation.element(at: 1) - elementAt1.expectNext([2]) + elementAt1.expectComplete(after: [2]) } func testFilter() { let operation = Signal.sequence([1, 2, 3]) let filtered = operation.filter { $0 % 2 != 0 } - filtered.expectNext([1, 3]) + filtered.expectComplete(after: [1, 3]) } func testFirst() { let operation = Signal.sequence([1, 2, 3]) let first = operation.first() - first.expectNext([1]) + first.expectComplete(after: [1]) } func testIgnoreElement() { let operation = Signal.sequence([1, 2, 3]) let ignoreElements = operation.ignoreElements() - ignoreElements.expectNext([]) + ignoreElements.expectComplete(after: []) } func testLast() { let operation = Signal.sequence([1, 2, 3]) let first = operation.last() - first.expectNext([3]) + first.expectComplete(after: [3]) } // TODO: sample @@ -196,39 +196,39 @@ class SignalTests: XCTestCase { func testSkip() { let operation = Signal.sequence([1, 2, 3]) let skipped1 = operation.skip(first: 1) - skipped1.expectNext([2, 3]) + skipped1.expectComplete(after: [2, 3]) } func testSkipLast() { let operation = Signal.sequence([1, 2, 3]) let skippedLast1 = operation.skip(last: 1) - skippedLast1.expectNext([1, 2]) + skippedLast1.expectComplete(after: [1, 2]) } func testTake() { let operation = Signal.sequence([1, 2, 3]) let taken2 = operation.take(first: 2) - taken2.expectNext([1, 2]) + taken2.expectComplete(after: [1, 2]) } func testTakeLast() { let operation = Signal.sequence([1, 2, 3]) let takenLast2 = operation.take(last: 2) - takenLast2.expectNext([2, 3]) + takenLast2.expectComplete(after: [2, 3]) } // func testThrottle() { // let operation = Signal.interval(0.4, queue: Queue.global).take(5) // let distinct = operation.throttle(1) // let exp = expectation(withDescription: "completed") -// distinct.expectNext([0, 3], expectation: exp) +// distinct.expectComplete(after: [0, 3], expectation: exp) // waitForExpectationsWithTimeout(3, handler: nil) // } func testIgnoreNil() { let operation = Signal.sequence(Array([1, nil, 3])) let unwrapped = operation.ignoreNil() - unwrapped.expectNext([1, 3]) + unwrapped.expectComplete(after: [1, 3]) } func testCombineLatestWith() { @@ -240,7 +240,7 @@ class SignalTests: XCTestCase { let combined = operationA.combineLatest(with: operationB).map { "\($0)\($1)" } let exp = expectation(description: "completed") - combined.expectNext(["1A", "1B", "2B", "3B", "3C"], expectation: exp) + combined.expectAsyncComplete(after: ["1A", "1B", "2B", "3B", "3C"], expectation: exp) bob.runOne() eve.runOne() @@ -259,7 +259,7 @@ class SignalTests: XCTestCase { let merged = operationA.merge(with: operationB) let exp = expectation(description: "completed") - merged.expectNext([1, 4, 5, 2, 6, 3], expectation: exp) + merged.expectAsyncComplete(after: [1, 4, 5, 2, 6, 3], expectation: exp) bob.runOne() eve.runOne() @@ -274,21 +274,28 @@ class SignalTests: XCTestCase { func testStartWith() { let operation = Signal.sequence([1, 2, 3]) let startWith4 = operation.start(with: 4) - startWith4.expectNext([4, 1, 2, 3]) + startWith4.expectComplete(after: [4, 1, 2, 3]) } func testZipWith() { let operationA = Signal.sequence([1, 2, 3]) let operationB = Signal.sequence(["A", "B"]) let combined = operationA.zip(with: operationB).map { "\($0)\($1)" } - combined.expectNext(["1A", "2B"], expectation: nil) + combined.expectComplete(after: ["1A", "2B"]) } func testZipWithWhenNotComplete() { let operationA = Signal.sequence([1, 2, 3]).ignoreTerminal() let operationB = Signal.sequence(["A", "B"]) let combined = operationA.zip(with: operationB).map { "\($0)\($1)" } - combined.expectNext(["1A", "2B"], expectation: nil) + combined.expectComplete(after: ["1A", "2B"]) + } + + func testZipWithWhenNotComplete2() { + let operationA = Signal.sequence([1, 2, 3]) + let operationB = Signal.sequence(["A", "B"]).ignoreTerminal() + let combined = operationA.zip(with: operationB).map { "\($0)\($1)" } + combined.expect(events: [.next("1A"), .next("2B")]) } func testZipWithAsyncSignal() { @@ -296,20 +303,20 @@ class SignalTests: XCTestCase { let operationB = Signal.interval(1.0).take(first: 10) // Takes 4 secs to emit 4 nexts. let combined = operationA.zip(with: operationB).map { $0 + $1 } // Completes after 4 nexts due to operationA and takes 4 secs due to operationB let exp = expectation(description: "completed") - combined.expectNext([0, 2, 4, 6], expectation: exp) + combined.expectAsyncComplete(after: [0, 2, 4, 6], expectation: exp) waitForExpectations(timeout: 5.0, handler: nil) } func testFlatMapError() { let operation = Signal.failed(.Error) let recovered = operation.flatMapError { error in Signal.just(1) } - recovered.expectNext([1]) + recovered.expectComplete(after: [1]) } func testFlatMapError2() { let operation = Signal.failed(.Error) let recovered = operation.flatMapError { error in Signal.just(1) } - recovered.expectNext([1]) + recovered.expectComplete(after: [1]) } func testRetry() { @@ -318,7 +325,7 @@ class SignalTests: XCTestCase { let operation = Signal.failed(.Error).executeIn(bob.context) let retry = operation.retry(times: 3) - retry.expect([.failed(.Error)]) + retry.expect(events: [.failed(.Error)]) XCTAssertEqual(bob.numberOfRuns, 4) } @@ -328,7 +335,7 @@ class SignalTests: XCTestCase { bob.runRemaining() let operation = Signal.sequence([1, 2, 3]).executeIn(bob.context) - operation.expectNext([1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) XCTAssertEqual(bob.numberOfRuns, 1) } @@ -358,7 +365,7 @@ class SignalTests: XCTestCase { bob.runRemaining() let operation = Signal.sequence([1, 2, 3]).observeIn(bob.context) - operation.expectNext([1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) XCTAssertEqual(bob.numberOfRuns, 4) // 3 elements + completion } @@ -369,7 +376,7 @@ class SignalTests: XCTestCase { let paused = operation.shareReplay().pausable(by: controller) let exp = expectation(description: "completed") - paused.expectNext([1, 3], expectation: exp) + paused.expectAsyncComplete(after: [1, 3], expectation: exp) operation.next(1) controller.next(false) @@ -383,13 +390,13 @@ class SignalTests: XCTestCase { func testTimeoutNoFailure() { let exp = expectation(description: "completed") - Signal.just(1).timeout(after: 0.2, with: .Error, on: DispatchQueue.main).expectNext([1], expectation: exp) + Signal.just(1).timeout(after: 0.2, with: .Error, on: DispatchQueue.main).expectAsyncComplete(after: [1], expectation: exp) waitForExpectations(timeout: 1, handler: nil) } func testTimeoutFailure() { let exp = expectation(description: "completed") - Signal.never().timeout(after: 0.5, with: .Error, on: DispatchQueue.main).expect([.failed(.Error)], expectation: exp) + Signal.never().timeout(after: 0.5, with: .Error, on: DispatchQueue.main).expectAsync(events: [.failed(.Error)], expectation: exp) waitForExpectations(timeout: 1, handler: nil) } @@ -402,7 +409,7 @@ class SignalTests: XCTestCase { let ambdWith = operationA.amb(with: operationB) let exp = expectation(description: "completed") - ambdWith.expectNext([3, 4], expectation: exp) + ambdWith.expectAsyncComplete(after: [3, 4], expectation: exp) eve.runOne() bob.runRemaining() @@ -414,7 +421,7 @@ class SignalTests: XCTestCase { func testCollect() { let operation = Signal.sequence([1, 2, 3]) let collected = operation.collect() - collected.expectNext([[1, 2, 3]]) + collected.expectComplete(after: [[1, 2, 3]]) } func testConcatWith() { @@ -426,7 +433,7 @@ class SignalTests: XCTestCase { let merged = operationA.concat(with: operationB) let exp = expectation(description: "completed") - merged.expectNext([1, 2, 3, 4], expectation: exp) + merged.expectAsyncComplete(after: [1, 2, 3, 4], expectation: exp) bob.runOne() eve.runOne() @@ -439,19 +446,19 @@ class SignalTests: XCTestCase { func testDefaultIfEmpty() { let operation = Signal.sequence([]) let defaulted = operation.defaultIfEmpty(1) - defaulted.expectNext([1]) + defaulted.expectComplete(after: [1]) } func testReduce() { let operation = Signal.sequence([1, 2, 3]) let reduced = operation.reduce(0, +) - reduced.expectNext([6]) + reduced.expectComplete(after: [6]) } func testZipPrevious() { let operation = Signal.sequence([1, 2, 3]) let zipped = operation.zipPrevious() - zipped.expectNext([(nil, 1), (1, 2), (2, 3)]) + zipped.expectComplete(after: [(nil, 1), (1, 2), (2, 3)]) } func testFlatMapMerge() { @@ -464,7 +471,7 @@ class SignalTests: XCTestCase { } let exp = expectation(description: "completed") - merged.expectNext([5, 10, 12, 6], expectation: exp) + merged.expectAsyncComplete(after: [5, 10, 12, 6], expectation: exp) bob.runOne() eves[0].runOne() @@ -485,7 +492,7 @@ class SignalTests: XCTestCase { } let exp = expectation(description: "completed") - merged.expectNext([5, 10, 12], expectation: exp) + merged.expectAsyncComplete(after: [5, 10, 12], expectation: exp) bob.runOne() eves[0].runOne() @@ -506,7 +513,7 @@ class SignalTests: XCTestCase { } let exp = expectation(description: "completed") - merged.expectNext([5, 6, 10, 12], expectation: exp) + merged.expectAsyncComplete(after: [5, 6, 10, 12], expectation: exp) bob.runRemaining() eves[1].runOne() @@ -523,10 +530,10 @@ class SignalTests: XCTestCase { let operation = Signal.sequence([1, 2, 3]).executeIn(bob.context) let replayed = operation.replay(2) - replayed.expectNext([1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) let _ = replayed.connect() - replayed.expectNext([2, 3]) - XCTAssertEqual(bob.numberOfRuns, 1) + replayed.expectComplete(after: [2, 3]) + XCTAssertEqual(bob.numberOfRuns, 2) } func testPublish() { @@ -536,10 +543,10 @@ class SignalTests: XCTestCase { let operation = Signal.sequence([1, 2, 3]).executeIn(bob.context) let published = operation.publish() - published.expectNext([1, 2, 3]) + operation.expectComplete(after: [1, 2, 3]) let _ = published.connect() - published.expectNext([]) + published.expectNoEvent() - XCTAssertEqual(bob.numberOfRuns, 1) + XCTAssertEqual(bob.numberOfRuns, 2) } }