diff --git a/CHANGELOG.md b/CHANGELOG.md index 8362872be..b6a18a484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ## Master * Adds `UIScrollView.willEndDragging` extension. #1365 +* Adds `materialize()` operator for RxBlocking's `BlockingObservable`. #1383 ## [3.6.1](https://github.com/ReactiveX/RxSwift/releases/tag/3.6.1) diff --git a/Documentation/UnitTests.md b/Documentation/UnitTests.md index f1376570d..7ec8a8a2e 100644 --- a/Documentation/UnitTests.md +++ b/Documentation/UnitTests.md @@ -86,7 +86,9 @@ It's easy to define `RxTests` extensions so you can write your tests in a readab It is also possible to write integration tests by using `RxBlocking` operators. -Importing operators from `RxBlocking` library will enable blocking the current thread and wait for sequence results. +Using `RxBlocking`'s `toBlocking()` method, you can block the current thread and wait for the sequence to complete, allowing you to synchronously access its result. + +A simple way to test the result of your sequence is using the `toArray` method. It will return an array of all elements emitted once a sequence has completed successfully, or `throw` if an error caused the sequence to terminate. ```swift let result = try fetchResource(location) @@ -95,3 +97,28 @@ let result = try fetchResource(location) XCTAssertEqual(result, expectedResult) ``` + +Another option would be to use the `materialize` operator which lets you more granularly examine your sequence. It will return a `MaterializedSequenceResult` enumeration that could be either `.completed` along with the emitted elements if the sequence completed successfully, or `failed` if the sequence terminated with an error, along with the emitted error. + +```swift +let result = try fetchResource(location) + .toBlocking() + .materialize() + +// For testing the results or error in the case of terminating with error +switch result { + case .completed: + XCTFail("Expected result to complete with error, but result was successful.") + case .failed(let elements, let error): + XCTAssertEqual(elements, expectedResult) + XCTAssertErrorEqual(error, expectedError) + } + +// For testing the results in the case of termination with completion +switch result { + case .completed(let elements): + XCTAssertEqual(elements, expectedResult) + case .failed(_, let error): + XCTFail("Expected result to complete without error, but received \(error).") + } +``` diff --git a/RxBlocking/BlockingObservable+Operators.swift b/RxBlocking/BlockingObservable+Operators.swift index 09e8c9ed2..f815ca3be 100644 --- a/RxBlocking/BlockingObservable+Operators.swift +++ b/RxBlocking/BlockingObservable+Operators.swift @@ -10,6 +10,19 @@ import RxSwift #endif +/// The `MaterializedSequenceResult` enum represents the materialized +/// output of a BlockingObservable. +/// +/// If the sequence terminates successfully, the result is represented +/// by `.completed` with the array of elements. +/// +/// If the sequene terminates with error, the result is represented +/// by `.failed` with both the array of elements and the terminating error. +public enum MaterializedSequenceResult { + case completed(elements: [T]) + case failed(elements: [T], error: Error) +} + extension BlockingObservable { /// Blocks current thread until sequence terminates. /// @@ -17,7 +30,8 @@ extension BlockingObservable { /// /// - returns: All elements of sequence. public func toArray() throws -> [E] { - return try convertToArray() + let results = materializeResult() + return try elementsOrThrow(results) } } @@ -28,7 +42,8 @@ extension BlockingObservable { /// /// - returns: First element of sequence. If sequence is empty `nil` is returned. public func first() throws -> E? { - return try convertToArray(max: 1).first + let results = materializeResult(max: 1) + return try elementsOrThrow(results).first } } @@ -39,7 +54,8 @@ extension BlockingObservable { /// /// - returns: Last element in the sequence. If sequence is empty `nil` is returned. public func last() throws -> E? { - return try convertToArray().last + let results = materializeResult() + return try elementsOrThrow(results).last } } @@ -60,7 +76,8 @@ extension BlockingObservable { /// - parameter predicate: A function to test each source element for a condition. /// - returns: Returns the only element of an sequence that satisfies the condition in the predicate, and reports an error if there is not exactly one element in the sequence. public func single(_ predicate: @escaping (E) throws -> Bool) throws -> E? { - let elements = try convertToArray(max: 2, predicate: predicate) + let results = materializeResult(max: 2, predicate: predicate) + let elements = try elementsOrThrow(results) switch elements.count { case 0: @@ -74,9 +91,19 @@ extension BlockingObservable { } extension BlockingObservable { - fileprivate func convertToArray(max: Int? = nil, predicate: @escaping (E) throws -> Bool = { _ in true }) throws -> [E] { + /// Blocks current thread until sequence terminates. + /// + /// The sequence is materialized as a result type capturing how the sequence terminated (completed or error), along with any elements up to that point. + /// + /// - returns: On completion, returns the list of elements in the sequence. On error, returns the list of elements up to that point, along with the error itself. + public func materialize() -> MaterializedSequenceResult { + return materializeResult() + } +} + +extension BlockingObservable { + fileprivate func materializeResult(max: Int? = nil, predicate: @escaping (E) throws -> Bool = { _ in true }) -> MaterializedSequenceResult { var elements: [E] = Array() - var error: Swift.Error? let lock = RunLoopLock(timeout: timeout) @@ -127,9 +154,18 @@ extension BlockingObservable { } if let error = error { - throw error + return MaterializedSequenceResult.failed(elements: elements, error: error) } - return elements + return MaterializedSequenceResult.completed(elements: elements) + } + + fileprivate func elementsOrThrow(_ results: MaterializedSequenceResult) throws -> [E] { + switch results { + case .failed(_, let error): + throw error + case .completed(let elements): + return elements + } } } diff --git a/RxBlocking/README.md b/RxBlocking/README.md index a49b2e543..b3f49969f 100644 --- a/RxBlocking/README.md +++ b/RxBlocking/README.md @@ -23,6 +23,10 @@ extension BlockingObservable { public func single() throws -> E? {} public func single(_ predicate: @escaping (E) throws -> Bool) throws -> E? {} } + +extension BlockingObservable { + public func materialize() -> MaterializedSequenceResult +} ``` diff --git a/Sources/AllTestz/main.swift b/Sources/AllTestz/main.swift index d180de466..039658d0c 100644 --- a/Sources/AllTestz/main.swift +++ b/Sources/AllTestz/main.swift @@ -238,6 +238,10 @@ final class ObservableBlockingTest_ : ObservableBlockingTest, RxTestCase { ("testSingle_independent", ObservableBlockingTest.testSingle_independent), ("testSingle_timeout", ObservableBlockingTest.testSingle_timeout), ("testSinglePredicate_timeout", ObservableBlockingTest.testSinglePredicate_timeout), + ("testMaterialize_empty", ObservableBlockingTest.testMaterialize_empty), + ("testMaterialize_empty_fail", ObservableBlockingTest.testMaterialize_empty_fail), + ("testMaterialize_someData", ObservableBlockingTest.testMaterialize_someData), + ("testMaterialize_someData_fail", ObservableBlockingTest.testMaterialize_someData_fail), ] } } diff --git a/Tests/RxBlockingTests/Observable+BlockingTest.swift b/Tests/RxBlockingTests/Observable+BlockingTest.swift index ddb354b23..9215dccf6 100644 --- a/Tests/RxBlockingTests/Observable+BlockingTest.swift +++ b/Tests/RxBlockingTests/Observable+BlockingTest.swift @@ -337,3 +337,54 @@ extension ObservableBlockingTest { } } } + +// materialize + +extension ObservableBlockingTest { + func testMaterialize_empty() { + let result = Observable.empty().toBlocking().materialize() + + switch result { + case .completed(let elements): + XCTAssertEqual(elements, []) + case .failed: + XCTFail("Expected result to be complete successfully, but result was failed.") + } + } + + func testMaterialize_empty_fail() { + let result = Observable.error(testError).toBlocking().materialize() + + switch result { + case .completed: + XCTFail("Expected result to be complete eith error, but result was successful.") + case .failed(let elements, let error): + XCTAssertEqual(elements, []) + XCTAssertErrorEqual(error, testError) + } + } + + func testMaterialize_someData() { + let result = Observable.of(42, 43, 44, 45).toBlocking().materialize() + + switch result { + case .completed(let elements): + XCTAssertEqual(elements, [42, 43, 44, 45]) + case .failed: + XCTFail("Expected result to be complete successfully, but result was failed.") + } + } + + func testMaterialize_someData_fail() { + let sequence = Observable.concat(Observable.of(42, 43, 44, 45), Observable.error(testError)) + let result = sequence.toBlocking().materialize() + + switch result { + case .completed: + XCTFail("Expected result to be complete eith error, but result was successful.") + case .failed(let elements, let error): + XCTAssertEqual(elements, [42, 43, 44, 45]) + XCTAssertErrorEqual(error, testError) + } + } +}