├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CombineExpectations.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── CombineExpectations │ ├── PublisherExpectation.swift │ ├── PublisherExpectations │ ├── AvailableElements.swift │ ├── Finished.swift │ ├── Inverted.swift │ ├── Map.swift │ ├── Next.swift │ ├── NextOne.swift │ ├── Prefix.swift │ └── Recording.swift │ ├── Recorder.swift │ └── RecordingError.swift └── Tests ├── CombineExpectationsTests ├── DocumentationTests.swift ├── FailureTestCase.swift ├── LateSubscriptionTest.swift ├── RecorderTests.swift ├── Support.swift ├── WackySubscriberTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [groue] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | build-test: 12 | name: Build & Test (Swift ${{ matrix.swift }}, ${{ matrix.platform }}) 13 | runs-on: macos-${{ matrix.macos || '11' }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | swift: ['5.1', '5.2', '5.3', '5.4', '5.5'] 18 | platform: [macOS, iOS, tvOS, watchOS] 19 | exclude: 20 | # watchOS requires Swift 5.4 or later. 21 | - swift: '5.1' 22 | platform: watchOS 23 | - swift: '5.2' 24 | platform: watchOS 25 | - swift: '5.3' 26 | platform: watchOS 27 | include: 28 | # The macOS 11 runner no longer includes Swift 5.1. 29 | - swift: '5.1' 30 | macos: '10.15' 31 | steps: 32 | - name: Clone 33 | uses: actions/checkout@v2 34 | - name: Build & Test 35 | uses: mxcl/xcodebuild@v1 36 | with: 37 | swift: ~${{ matrix.swift }} 38 | platform: ${{ matrix.platform }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | /.swiftpm 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | #### 0.x Releases 7 | 8 | - [0.10.0](#0100) 9 | - [0.9.0](#090) 10 | - [0.8.0](#080) 11 | - [0.7.0](#070) 12 | - [0.6.0](#060) 13 | - [0.5.0](#050) 14 | - [0.4.0](#040) 15 | - [0.3.0](#030) 16 | - [0.2.0](#020) 17 | - [0.1.0](#010) 18 | 19 | ## 0.10.0 20 | 21 | Released August 11, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.9.0...v0.10.0) 22 | 23 | - **New**: [#17](https://github.com/groue/CombineExpectations/pull/17) by [@chris-araman](https://github.com/chris-araman): Improvements for Swift 5.1, Swift 5.3, Swift 5.4, watchOS 7.4, Xcode 12.5, Xcode 13 beta 24 | 25 | ## 0.9.0 26 | 27 | Released June 7, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.8.0...v0.9.0) 28 | 29 | - **New**: [#16](https://github.com/groue/CombineExpectations/pull/16) by [@chris-araman](https://github.com/chris-araman): Support watchOS 6 or later when building with Swift 5.4 or later 30 | 31 | ## 0.8.0 32 | 33 | Released May 29, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.7.0...v0.8.0) 34 | 35 | - **Fixed**: [#15](https://github.com/groue/CombineExpectations/pull/15) by [@chrisballinger](https://github.com/chrisballinger): Fix XCTFail not found issue with Xcode 12.5 36 | 37 | ## 0.7.0 38 | 39 | Released January 9, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.6.0...v0.7.0) 40 | 41 | - **Fixed**: [#13](https://github.com/groue/CombineExpectations/pull/13) by [@chrisballinger](https://github.com/chrisballinger): Remove module_name override in CocoaPods spec 42 | 43 | ## 0.6.0 44 | 45 | Released December 23, 2020 • [diff](https://github.com/groue/CombineExpectations/compare/v0.5.0...v0.6.0) 46 | 47 | - **New**: [#11](https://github.com/groue/CombineExpectations/pull/11): `availableElements` expectation (fixes [#8](https://github.com/groue/CombineExpectations/issues/8)). 48 | 49 | ## 0.5.0 50 | 51 | Released June 25, 2020 • [diff](https://github.com/groue/CombineExpectations/compare/v0.4.0...v0.5.0) 52 | 53 | - **Fixed**: `next().get()` no longer returns an optional. 54 | - **New**: Support for Xcode 12 55 | 56 | ## 0.4.0 57 | 58 | Released January 4, 2020 • [diff](https://github.com/groue/CombineExpectations/compare/v0.3.0...v0.4.0) 59 | 60 | - [#6](https://github.com/groue/CombineExpectations/pull/6): Support for synchronous tests 61 | 62 | **Documentation diff**: 63 | 64 | The [Usage] section shows how to use the new `get()` method in order to perform synchronous tests that do not have to wait. 65 | 66 | 67 | ## 0.3.0 68 | 69 | Released November 27, 2019 • [diff](https://github.com/groue/CombineExpectations/compare/v0.2.0...v0.3.0) 70 | 71 | - [#2](https://github.com/groue/CombineExpectations/pull/2): RecordingError renaming 72 | - [#3](https://github.com/groue/CombineExpectations/pull/3): Next Expectation 73 | - [#4](https://github.com/groue/CombineExpectations/pull/4): Drop the "first" expectation 74 | 75 | 76 | ## 0.2.0 77 | 78 | Released November 24, 2019 • [diff](https://github.com/groue/CombineExpectations/compare/v0.1.0...v0.2.0) 79 | 80 | **Increased robustness** 81 | 82 | ## 0.1.0 83 | 84 | Released November 23, 2019 85 | 86 | **Initial release** 87 | 88 | [Usage]: README.md#usage 89 | -------------------------------------------------------------------------------- /CombineExpectations.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'CombineExpectations' 3 | s.version = '0.10.0' 4 | 5 | s.license = { :type => 'MIT', :file => 'LICENSE' } 6 | s.summary = 'A set of extensions for SQLite, GRDB.swift, and Combine' 7 | s.homepage = 'https://github.com/groue/CombineExpectations' 8 | s.author = { 'Gwendal Roué' => 'gr@pierlis.com' } 9 | s.source = { :git => 'https://github.com/groue/CombineExpectations.git', :tag => "v#{s.version}" } 10 | 11 | s.swift_versions = ['5.1', '5.2', '5.3', '5.4'] 12 | s.ios.deployment_target = '13.0' 13 | s.osx.deployment_target = '10.15' 14 | s.tvos.deployment_target = '13.0' 15 | s.watchos.deployment_target = '7.4' 16 | 17 | s.frameworks = ['Combine', 'XCTest'] 18 | s.source_files = 'Sources/CombineExpectations/**/*.swift' 19 | s.pod_target_xcconfig = { 20 | "ENABLE_TESTING_SEARCH_PATHS" => "YES" # Required for Xcode 12.5 21 | } 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 Gwendal Roué 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CombineExpectations", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .tvOS(.v13), 12 | ], 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "CombineExpectations", 17 | targets: ["CombineExpectations"]), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "CombineExpectations", 24 | dependencies: [], 25 | linkerSettings: [.linkedFramework("XCTest")]), 26 | .testTarget( 27 | name: "CombineExpectationsTests", 28 | dependencies: ["CombineExpectations"]), 29 | ] 30 | ) 31 | 32 | #if swift(>=5.4) 33 | // XCTest was introduced for watchOS with Swift 5.4, Xcode 12.5, and watchOS 7.4. 34 | package.platforms! += [ 35 | .watchOS("7.4") 36 | ] 37 | #endif 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Combine Expectations 2 | 3 | ### Utilities for tests that wait for Combine publishers. 4 | 5 | --- 6 | 7 | **Latest release**: [version 0.10.0](https://github.com/groue/CombineExpectations/tree/v0.10.0) (August 11, 2021) • [Release Notes] 8 | 9 | **Requirements**: iOS 13+, macOS 10.15+, and tvOS 13+ require Swift 5.1+ or Xcode 11+. watchOS 7.4+ requires Swift 5.4+ or Xcode 12.5+. 10 | 11 | **Contact**: Report bugs and ask questions in [Github issues](https://github.com/groue/CombineExpectations/issues). 12 | 13 | --- 14 | 15 | Testing Combine publishers with [XCTestExpectation](*https://developer.apple.com/documentation/xctest/xctestexpectation*) often requires setting up a lot of boilerplate code. 16 | 17 | CombineExpectations aims at streamlining those tests. It defines an XCTestCase method which waits for *publisher expectations*. 18 | 19 | - [Usage] 20 | - [Installation] 21 | - [Publisher Expectations]: [availableElements], [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single] 22 | 23 | --- 24 | 25 | ## Usage 26 | 27 | Waiting for [Publisher Expectations] allows your tests to look like this: 28 | 29 | ```swift 30 | import XCTest 31 | import CombineExpectations 32 | 33 | class PublisherTests: XCTestCase { 34 | func testElements() throws { 35 | // 1. Create a publisher 36 | let publisher = ... 37 | 38 | // 2. Start recording the publisher 39 | let recorder = publisher.record() 40 | 41 | // 3. Wait for a publisher expectation 42 | let elements = try wait(for: recorder.elements, timeout: ..., description: "Elements") 43 | 44 | // 4. Test the result of the expectation 45 | XCTAssertEqual(elements, ["Hello", "World!"]) 46 | } 47 | } 48 | ``` 49 | 50 | **When you wait for a publisher expectation:** 51 | 52 | - The test fails if the expectation is not fulfilled within the specified timeout. 53 | - An error is thrown if the expected value can not be returned. For example, waiting for `recorder.elements` throws the publisher error if the publisher completes with a failure. 54 | - The `wait` method returns immediately if the expectation has already reached the waited state. 55 | 56 | You can wait multiple times for a publisher: 57 | 58 | ```swift 59 | class PublisherTests: XCTestCase { 60 | func testPublisher() throws { 61 | let publisher = ... 62 | let recorder = publisher.record() 63 | 64 | // Wait for first element 65 | _ = try wait(for: recorder.next(), timeout: ...) 66 | 67 | // Wait for second element 68 | _ = try wait(for: recorder.next(), timeout: ...) 69 | 70 | // Wait for successful completion 71 | try wait(for: recorder.finished, timeout: ...) 72 | } 73 | } 74 | ``` 75 | 76 | **Not all tests have to wait**, because some publishers expectations are fulfilled right away. In this case, prefer the synchronous `get()` method over `wait(for:timeout:)`, as below: 77 | 78 | ```swift 79 | class PublisherTests: XCTestCase { 80 | func testSynchronousPublisher() throws { 81 | // 1. Create a publisher 82 | let publisher = ... 83 | 84 | // 2. Start recording the publisher 85 | let recorder = publisher.record() 86 | 87 | // 3. Grab the expected result 88 | let elements = try recorder.elements.get() 89 | 90 | // 4. Test the result of the expectation 91 | XCTAssertEqual(elements, ["Hello", "World!"]) 92 | } 93 | } 94 | ``` 95 | 96 | Just like `wait(for:timeout:)`, the `get()` method can be called multiple times: 97 | 98 | ```swift 99 | class PublisherTests: XCTestCase { 100 | // SUCCESS: no error 101 | func testPassthroughSubjectSynchronouslyPublishesElements() throws { 102 | let publisher = PassthroughSubject() 103 | let recorder = publisher.record() 104 | 105 | publisher.send("foo") 106 | try XCTAssertEqual(recorder.next().get(), "foo") 107 | 108 | publisher.send("bar") 109 | try XCTAssertEqual(recorder.next().get(), "bar") 110 | } 111 | } 112 | ``` 113 | 114 | 115 | ## Installation 116 | 117 | Add a dependency for CombineExpectations to your [Swift Package](https://swift.org/package-manager/) test targets: 118 | 119 | ```diff 120 | import PackageDescription 121 | 122 | let package = Package( 123 | dependencies: [ 124 | + .package(url: "https://github.com/groue/CombineExpectations.git", ...) 125 | ], 126 | targets: [ 127 | .testTarget( 128 | dependencies: [ 129 | + "CombineExpectations" 130 | ]) 131 | ] 132 | ) 133 | ``` 134 | 135 | 136 | ## Publisher Expectations 137 | 138 | There are various publisher expectations. Each one waits for a specific publisher aspect: 139 | 140 | - [availableElements]: all published elements until timeout expiration 141 | - [completion]: the publisher completion 142 | - [elements]: all published elements until successful completion 143 | - [finished]: the publisher successful completion 144 | - [last]: the last published element 145 | - [next()]: the next published element 146 | - [next(count)]: the next N published elements 147 | - [prefix(maxLength)]: the first N published elements 148 | - [recording]: the full recording of publisher events 149 | - [single]: the one and only published element 150 | 151 | --- 152 | 153 | ### availableElements 154 | 155 | :clock230: `recorder.availableElements` waits for the expectation to expire, or the recorded publisher to complete. 156 | 157 | :x: When waiting for this expectation, the publisher error is thrown if the publisher fails before the expectation has expired. 158 | 159 | :white_check_mark: Otherwise, an array of all elements published before the expectation has expired is returned. 160 | 161 | :arrow_right: Related expectations: [elements], [prefix(maxLength)]. 162 | 163 | Unlike other expectations, `availableElements` does not make a test fail on timeout expiration. It just returns the elements published so far. 164 | 165 | Example: 166 | 167 | ```swift 168 | // SUCCESS: no timeout, no error 169 | func testTimerPublishesIncreasingDates() throws { 170 | let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() 171 | let recorder = publisher.record() 172 | let dates = try wait(for: recorder.availableElements, timeout: ...) 173 | XCTAssertEqual(dates.sorted(), dates) 174 | } 175 | ``` 176 | 177 | ### completion 178 | 179 | :clock230: `recorder.completion` waits for the recorded publisher to complete. 180 | 181 | :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time. 182 | 183 | :white_check_mark: Otherwise, a [`Subscribers.Completion`](https://developer.apple.com/documentation/combine/subscribers/completion) is returned. 184 | 185 | :arrow_right: Related expectations: [finished], [recording]. 186 | 187 | Example: 188 | 189 | ```swift 190 | // SUCCESS: no timeout, no error 191 | func testArrayPublisherCompletesWithSuccess() throws { 192 | let publisher = ["foo", "bar", "baz"].publisher 193 | let recorder = publisher.record() 194 | let completion = try wait(for: recorder.completion, timeout: ...) 195 | if case let .failure(error) = completion { 196 | XCTFail("Unexpected error \(error)") 197 | } 198 | } 199 | 200 | // SUCCESS: no error 201 | func testArrayPublisherSynchronouslyCompletesWithSuccess() throws { 202 | let publisher = ["foo", "bar", "baz"].publisher 203 | let recorder = publisher.record() 204 | let completion = try recorder.completion.get() 205 | if case let .failure(error) = completion { 206 | XCTFail("Unexpected error \(error)") 207 | } 208 | } 209 | ``` 210 | 211 |
212 | Examples of failing tests 213 | 214 | ```swift 215 | // FAIL: Asynchronous wait failed 216 | // FAIL: Caught error RecordingError.notCompleted 217 | func testCompletionTimeout() throws { 218 | let publisher = PassthroughSubject() 219 | let recorder = publisher.record() 220 | let completion = try wait(for: recorder.completion, timeout: ...) 221 | } 222 | ``` 223 | 224 |
225 | 226 | 227 | --- 228 | 229 | ### elements 230 | 231 | :clock230: `recorder.elements` waits for the recorded publisher to complete. 232 | 233 | :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails. 234 | 235 | :white_check_mark: Otherwise, an array of published elements is returned. 236 | 237 | :arrow_right: Related expectations: [availableElements], [last], [prefix(maxLength)], [recording], [single]. 238 | 239 | Example: 240 | 241 | ```swift 242 | // SUCCESS: no timeout, no error 243 | func testArrayPublisherPublishesArrayElements() throws { 244 | let publisher = ["foo", "bar", "baz"].publisher 245 | let recorder = publisher.record() 246 | let elements = try wait(for: recorder.elements, timeout: ...) 247 | XCTAssertEqual(elements, ["foo", "bar", "baz"]) 248 | } 249 | 250 | // SUCCESS: no error 251 | func testArrayPublisherSynchronouslyPublishesArrayElements() throws { 252 | let publisher = ["foo", "bar", "baz"].publisher 253 | let recorder = publisher.record() 254 | let elements = try recorder.elements.get() 255 | XCTAssertEqual(elements, ["foo", "bar", "baz"]) 256 | } 257 | ``` 258 | 259 |
260 | Examples of failing tests 261 | 262 | ```swift 263 | // FAIL: Asynchronous wait failed 264 | // FAIL: Caught error RecordingError.notCompleted 265 | func testElementsTimeout() throws { 266 | let publisher = PassthroughSubject() 267 | let recorder = publisher.record() 268 | let elements = try wait(for: recorder.elements, timeout: ...) 269 | } 270 | 271 | // FAIL: Caught error MyError 272 | func testElementsError() throws { 273 | let publisher = PassthroughSubject() 274 | let recorder = publisher.record() 275 | publisher.send(completion: .failure(MyError())) 276 | let elements = try wait(for: recorder.elements, timeout: ...) 277 | } 278 | ``` 279 | 280 |
281 | 282 | 283 | --- 284 | 285 | ### finished 286 | 287 | :clock230: `recorder.finished` waits for the recorded publisher to complete. 288 | 289 | :x: When waiting for this expectation, the publisher error is thrown if the publisher fails. 290 | 291 | :arrow_right: Related expectations: [completion], [recording]. 292 | 293 | Example: 294 | 295 | ```swift 296 | // SUCCESS: no timeout, no error 297 | func testArrayPublisherFinishesWithoutError() throws { 298 | let publisher = ["foo", "bar", "baz"].publisher 299 | let recorder = publisher.record() 300 | try wait(for: recorder.finished, timeout: ...) 301 | } 302 | 303 | // SUCCESS: no error 304 | func testArrayPublisherSynchronouslyFinishesWithoutError() throws { 305 | let publisher = ["foo", "bar", "baz"].publisher 306 | let recorder = publisher.record() 307 | try recorder.finished.get() 308 | } 309 | ``` 310 | 311 |
312 | Examples of failing tests 313 | 314 | ```swift 315 | // FAIL: Asynchronous wait failed 316 | func testFinishedTimeout() throws { 317 | let publisher = PassthroughSubject() 318 | let recorder = publisher.record() 319 | try wait(for: recorder.finished, timeout: ...) 320 | } 321 | 322 | // FAIL: Caught error MyError 323 | func testFinishedError() throws { 324 | let publisher = PassthroughSubject() 325 | let recorder = publisher.record() 326 | publisher.send(completion: .failure(MyError())) 327 | try wait(for: recorder.finished, timeout: ...) 328 | } 329 | ``` 330 | 331 |
332 | 333 | `recorder.finished` can be inverted: 334 | 335 | ```swift 336 | // SUCCESS: no timeout, no error 337 | func testPassthroughSubjectDoesNotFinish() throws { 338 | let publisher = PassthroughSubject() 339 | let recorder = publisher.record() 340 | try wait(for: recorder.finished.inverted, timeout: ...) 341 | } 342 | ``` 343 | 344 |
345 | Examples of failing tests 346 | 347 | ```swift 348 | // FAIL: Fulfilled inverted expectation 349 | // FAIL: Caught error MyError 350 | func testInvertedFinishedError() throws { 351 | let publisher = PassthroughSubject() 352 | let recorder = publisher.record() 353 | publisher.send(completion: .failure(MyError())) 354 | try wait(for: recorder.finished.inverted, timeout: ...) 355 | } 356 | ``` 357 | 358 |
359 | 360 | 361 | --- 362 | 363 | ### last 364 | 365 | :clock230: `recorder.last` waits for the recorded publisher to complete. 366 | 367 | :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails. 368 | 369 | :white_check_mark: Otherwise, the last published element is returned, or nil if the publisher completes before it publishes any element. 370 | 371 | :arrow_right: Related expectations: [elements], [single]. 372 | 373 | Example: 374 | 375 | ```swift 376 | // SUCCESS: no timeout, no error 377 | func testArrayPublisherPublishesLastElementLast() throws { 378 | let publisher = ["foo", "bar", "baz"].publisher 379 | let recorder = publisher.record() 380 | if let element = try wait(for: recorder.last, timeout: ...) { 381 | XCTAssertEqual(element, "baz") 382 | } else { 383 | XCTFail("Expected one element") 384 | } 385 | } 386 | 387 | // SUCCESS: no error 388 | func testArrayPublisherSynchronouslyPublishesLastElementLast() throws { 389 | let publisher = ["foo", "bar", "baz"].publisher 390 | let recorder = publisher.record() 391 | if let element = try recorder.last.get() { 392 | XCTAssertEqual(element, "baz") 393 | } else { 394 | XCTFail("Expected one element") 395 | } 396 | } 397 | ``` 398 | 399 |
400 | Examples of failing tests 401 | 402 | ```swift 403 | // FAIL: Asynchronous wait failed 404 | // FAIL: Caught error RecordingError.notCompleted 405 | func testLastTimeout() throws { 406 | let publisher = PassthroughSubject() 407 | let recorder = publisher.record() 408 | let element = try wait(for: recorder.last, timeout: ...) 409 | } 410 | 411 | // FAIL: Caught error MyError 412 | func testLastError() throws { 413 | let publisher = PassthroughSubject() 414 | let recorder = publisher.record() 415 | publisher.send(completion: .failure(MyError())) 416 | let element = try wait(for: recorder.last, timeout: ...) 417 | } 418 | ``` 419 | 420 |
421 | 422 | 423 | --- 424 | 425 | ### next() 426 | 427 | :clock230: `recorder.next()` waits for the recorded publisher to emit one element, or to complete. 428 | 429 | :x: When waiting for this expectation, a `RecordingError.notEnoughElements` is thrown if the publisher does not publish one element after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next element. 430 | 431 | :white_check_mark: Otherwise, the next published element is returned. 432 | 433 | :arrow_right: Related expectations: [next(count)], [single]. 434 | 435 | Example: 436 | 437 | ```swift 438 | // SUCCESS: no timeout, no error 439 | func testArrayOfTwoElementsPublishesElementsInOrder() throws { 440 | let publisher = ["foo", "bar"].publisher 441 | let recorder = publisher.record() 442 | 443 | var element = try wait(for: recorder.next(), timeout: ...) 444 | XCTAssertEqual(element, "foo") 445 | 446 | element = try wait(for: recorder.next(), timeout: ...) 447 | XCTAssertEqual(element, "bar") 448 | } 449 | 450 | // SUCCESS: no error 451 | func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws { 452 | let publisher = ["foo", "bar"].publisher 453 | let recorder = publisher.record() 454 | 455 | var element = try recorder.next().get() 456 | XCTAssertEqual(element, "foo") 457 | 458 | element = try recorder.next().get() 459 | XCTAssertEqual(element, "bar") 460 | } 461 | ``` 462 | 463 |
464 | Examples of failing tests 465 | 466 | ```swift 467 | // FAIL: Asynchronous wait failed 468 | // FAIL: Caught error RecordingError.notEnoughElements 469 | func testNextTimeout() throws { 470 | let publisher = PassthroughSubject() 471 | let recorder = publisher.record() 472 | let element = try wait(for: recorder.next(), timeout: ...) 473 | } 474 | 475 | // FAIL: Caught error MyError 476 | func testNextError() throws { 477 | let publisher = PassthroughSubject() 478 | let recorder = publisher.record() 479 | publisher.send(completion: .failure(MyError())) 480 | let element = try wait(for: recorder.next(), timeout: ...) 481 | } 482 | 483 | // FAIL: Caught error RecordingError.notEnoughElements 484 | func testNextNotEnoughElementsError() throws { 485 | let publisher = PassthroughSubject() 486 | let recorder = publisher.record() 487 | publisher.send(completion: .finished) 488 | let element = try wait(for: recorder.next(), timeout: ...) 489 | } 490 | ``` 491 | 492 |
493 | 494 | `recorder.next()` can be inverted: 495 | 496 | ```swift 497 | // SUCCESS: no timeout, no error 498 | func testPassthroughSubjectDoesNotPublishAnyElement() throws { 499 | let publisher = PassthroughSubject() 500 | let recorder = publisher.record() 501 | try wait(for: recorder.next().inverted, timeout: ...) 502 | } 503 | ``` 504 | 505 |
506 | Examples of failing tests 507 | 508 | ```swift 509 | // FAIL: Fulfilled inverted expectation 510 | func testInvertedNextTooEarly() throws { 511 | let publisher = PassthroughSubject() 512 | let recorder = publisher.record() 513 | publisher.send("foo") 514 | try wait(for: recorder.next().inverted, timeout: ...) 515 | } 516 | 517 | // FAIL: Fulfilled inverted expectation 518 | // FAIL: Caught error MyError 519 | func testInvertedNextError() throws { 520 | let publisher = PassthroughSubject() 521 | let recorder = publisher.record() 522 | publisher.send(completion: .failure(MyError())) 523 | try wait(for: recorder.next().inverted, timeout: ...) 524 | } 525 | ``` 526 | 527 |
528 | 529 | 530 | --- 531 | 532 | ### next(count) 533 | 534 | :clock230: `recorder.next(count)` waits for the recorded publisher to emit `count` elements, or to complete. 535 | 536 | :x: When waiting for this expectation, a `RecordingError.notEnoughElements` is thrown if the publisher does not publish `count` elements after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next `count` elements. 537 | 538 | :white_check_mark: Otherwise, an array of exactly `count` elements is returned. 539 | 540 | :arrow_right: Related expectations: [next()], [prefix(maxLength)]. 541 | 542 | Example: 543 | 544 | ```swift 545 | // SUCCESS: no timeout, no error 546 | func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { 547 | let publisher = ["foo", "bar", "baz"].publisher 548 | let recorder = publisher.record() 549 | 550 | var elements = try wait(for: recorder.next(2), timeout: ...) 551 | XCTAssertEqual(elements, ["foo", "bar"]) 552 | 553 | elements = try wait(for: recorder.next(1), timeout: ...) 554 | XCTAssertEqual(elements, ["baz"]) 555 | } 556 | 557 | // SUCCESS: no error 558 | func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws { 559 | let publisher = ["foo", "bar", "baz"].publisher 560 | let recorder = publisher.record() 561 | 562 | var elements = try recorder.next(2).get() 563 | XCTAssertEqual(elements, ["foo", "bar"]) 564 | 565 | elements = try recorder.next(1).get() 566 | XCTAssertEqual(elements, ["baz"]) 567 | } 568 | ``` 569 | 570 |
571 | Examples of failing tests 572 | 573 | ```swift 574 | // FAIL: Asynchronous wait failed 575 | // FAIL: Caught error RecordingError.notEnoughElements 576 | func testNextCountTimeout() throws { 577 | let publisher = PassthroughSubject() 578 | let recorder = publisher.record() 579 | publisher.send("foo") 580 | let elements = try wait(for: recorder.next(2), timeout: ...) 581 | } 582 | 583 | // FAIL: Caught error MyError 584 | func testNextCountError() throws { 585 | let publisher = PassthroughSubject() 586 | let recorder = publisher.record() 587 | publisher.send("foo") 588 | publisher.send(completion: .failure(MyError())) 589 | let elements = try wait(for: recorder.next(2), timeout: ...) 590 | } 591 | 592 | // FAIL: Caught error RecordingError.notEnoughElements 593 | func testNextCountNotEnoughElementsError() throws { 594 | let publisher = PassthroughSubject() 595 | let recorder = publisher.record() 596 | publisher.send("foo") 597 | publisher.send(completion: .finished) 598 | let elements = try wait(for: recorder.next(2), timeout: ...) 599 | } 600 | ``` 601 | 602 |
603 | 604 | 605 | --- 606 | 607 | ### prefix(maxLength) 608 | 609 | :clock230: `recorder.prefix(maxLength)` waits for the recorded publisher to emit `maxLength` elements, or to complete. 610 | 611 | :x: When waiting for this expectation, the publisher error is thrown if the publisher fails before `maxLength` elements are published. 612 | 613 | :white_check_mark: Otherwise, an array of received elements is returned, containing at most `maxLength` elements, or less if the publisher completes early. 614 | 615 | :arrow_right: Related expectations: [availableElements], [elements], [next(count)]. 616 | 617 | Example: 618 | 619 | ```swift 620 | // SUCCESS: no timeout, no error 621 | func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { 622 | let publisher = ["foo", "bar", "baz"].publisher 623 | let recorder = publisher.record() 624 | let elements = try wait(for: recorder.prefix(2), timeout: ...) 625 | XCTAssertEqual(elements, ["foo", "bar"]) 626 | } 627 | 628 | // SUCCESS: no error 629 | func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws { 630 | let publisher = ["foo", "bar", "baz"].publisher 631 | let recorder = publisher.record() 632 | let elements = try recorder.prefix(2).get() 633 | XCTAssertEqual(elements, ["foo", "bar"]) 634 | } 635 | ``` 636 | 637 |
638 | Examples of failing tests 639 | 640 | ```swift 641 | // FAIL: Asynchronous wait failed 642 | func testPrefixTimeout() throws { 643 | let publisher = PassthroughSubject() 644 | let recorder = publisher.record() 645 | publisher.send("foo") 646 | let elements = try wait(for: recorder.prefix(2), timeout: ...) 647 | } 648 | 649 | // FAIL: Caught error MyError 650 | func testPrefixError() throws { 651 | let publisher = PassthroughSubject() 652 | let recorder = publisher.record() 653 | publisher.send("foo") 654 | publisher.send(completion: .failure(MyError())) 655 | let elements = try wait(for: recorder.prefix(2), timeout: ...) 656 | } 657 | ``` 658 | 659 |
660 | 661 | `recorder.prefix(maxLength)` can be inverted: 662 | 663 | ```swift 664 | // SUCCESS: no timeout, no error 665 | func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { 666 | let publisher = PassthroughSubject() 667 | let recorder = publisher.record() 668 | publisher.send("foo") 669 | publisher.send("bar") 670 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) 671 | XCTAssertEqual(elements, ["foo", "bar"]) 672 | } 673 | ``` 674 | 675 |
676 | Examples of failing tests 677 | 678 | ```swift 679 | // FAIL: Fulfilled inverted expectation 680 | func testInvertedPrefixTooEarly() throws { 681 | let publisher = PassthroughSubject() 682 | let recorder = publisher.record() 683 | publisher.send("foo") 684 | publisher.send("bar") 685 | publisher.send("baz") 686 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) 687 | } 688 | 689 | // FAIL: Fulfilled inverted expectation 690 | // FAIL: Caught error MyError 691 | func testInvertedPrefixError() throws { 692 | let publisher = PassthroughSubject() 693 | let recorder = publisher.record() 694 | publisher.send("foo") 695 | publisher.send(completion: .failure(MyError())) 696 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) 697 | } 698 | ``` 699 | 700 |
701 | 702 | 703 | --- 704 | 705 | ### recording 706 | 707 | :clock230: `recorder.recording` waits for the recorded publisher to complete. 708 | 709 | :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time. 710 | 711 | :white_check_mark: Otherwise, a [`Record.Recording`](https://developer.apple.com/documentation/combine/record/recording) is returned. 712 | 713 | :arrow_right: Related expectations: [completion], [elements], [finished]. 714 | 715 | Example: 716 | 717 | ```swift 718 | // SUCCESS: no timeout, no error 719 | func testArrayPublisherRecording() throws { 720 | let publisher = ["foo", "bar", "baz"].publisher 721 | let recorder = publisher.record() 722 | let recording = try wait(for: recorder.recording, timeout: ...) 723 | XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 724 | if case let .failure(error) = recording.completion { 725 | XCTFail("Unexpected error \(error)") 726 | } 727 | } 728 | 729 | // SUCCESS: no error 730 | func testArrayPublisherSynchronousRecording() throws { 731 | let publisher = ["foo", "bar", "baz"].publisher 732 | let recorder = publisher.record() 733 | let recording = try recorder.recording.get() 734 | XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 735 | if case let .failure(error) = recording.completion { 736 | XCTFail("Unexpected error \(error)") 737 | } 738 | } 739 | ``` 740 | 741 |
742 | Examples of failing tests 743 | 744 | ```swift 745 | // FAIL: Asynchronous wait failed 746 | // FAIL: Caught error RecordingError.notCompleted 747 | func testRecordingTimeout() throws { 748 | let publisher = PassthroughSubject() 749 | let recorder = publisher.record() 750 | let recording = try wait(for: recorder.recording, timeout: ...) 751 | } 752 | ``` 753 | 754 |
755 | 756 | 757 | --- 758 | 759 | ### single 760 | 761 | :clock230: `recorder.single` waits for the recorded publisher to complete. 762 | 763 | :x: When waiting for this expectation, a `RecordingError` is thrown if the publisher does not complete on time, or does not publish exactly one element before it completes. The publisher error is thrown if the publisher fails. 764 | 765 | :white_check_mark: Otherwise, the single published element is returned. 766 | 767 | :arrow_right: Related expectations: [elements], [last], [next()]. 768 | 769 | Example: 770 | 771 | ```swift 772 | // SUCCESS: no timeout, no error 773 | func testJustPublishesExactlyOneElement() throws { 774 | let publisher = Just("foo") 775 | let recorder = publisher.record() 776 | let element = try wait(for: recorder.single, timeout: ...) 777 | XCTAssertEqual(element, "foo") 778 | } 779 | 780 | // SUCCESS: no error 781 | func testJustSynchronouslyPublishesExactlyOneElement() throws { 782 | let publisher = Just("foo") 783 | let recorder = publisher.record() 784 | let element = try recorder.single.get() 785 | XCTAssertEqual(element, "foo") 786 | } 787 | ``` 788 | 789 |
790 | Examples of failing tests 791 | 792 | ```swift 793 | // FAIL: Asynchronous wait failed 794 | // FAIL: Caught error RecordingError.notCompleted 795 | func testSingleTimeout() throws { 796 | let publisher = PassthroughSubject() 797 | let recorder = publisher.record() 798 | let element = try wait(for: recorder.single, timeout: ...) 799 | } 800 | 801 | // FAIL: Caught error MyError 802 | func testSingleError() throws { 803 | let publisher = PassthroughSubject() 804 | let recorder = publisher.record() 805 | publisher.send(completion: .failure(MyError())) 806 | let element = try wait(for: recorder.single, timeout: ...) 807 | } 808 | 809 | // FAIL: Caught error RecordingError.tooManyElements 810 | func testSingleTooManyElementsError() throws { 811 | let publisher = PassthroughSubject() 812 | let recorder = publisher.record() 813 | publisher.send("foo") 814 | publisher.send("bar") 815 | publisher.send(completion: .finished) 816 | let element = try wait(for: recorder.single, timeout: ...) 817 | } 818 | 819 | // FAIL: Caught error RecordingError.notEnoughElements 820 | func testSingleNotEnoughElementsError() throws { 821 | let publisher = PassthroughSubject() 822 | let recorder = publisher.record() 823 | publisher.send(completion: .finished) 824 | let element = try wait(for: recorder.single, timeout: ...) 825 | } 826 | ``` 827 | 828 |
829 | 830 | 831 | [Release Notes]: CHANGELOG.md 832 | [Usage]: #usage 833 | [Installation]: #installation 834 | [Publisher Expectations]: #publisher-expectations 835 | [finished]: #finished 836 | [prefix(maxLength)]: #prefixmaxlength 837 | [next()]: #next 838 | [next(count)]: #nextcount 839 | [recording]: #recording 840 | [completion]: #completion 841 | [elements]: #elements 842 | [last]: #last 843 | [single]: #single 844 | [availableElements]: #availableElements 845 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectation.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | /// A name space for publisher expectations 4 | public enum PublisherExpectations { } 5 | 6 | /// The base protocol for PublisherExpectation. It is an implementation detail 7 | /// that you are not supposed to use, as shown by the underscore prefix. 8 | /// 9 | /// :nodoc: 10 | public protocol _PublisherExpectationBase { 11 | /// Sets up an XCTestExpectation. This method is an implementation detail 12 | /// that you are not supposed to use, as shown by the underscore prefix. 13 | func _setup(_ expectation: XCTestExpectation) 14 | 15 | /// Returns an object that waits for the expectation. If nil, expectation 16 | /// is waited by the XCTestCase. 17 | func _makeWaiter() -> XCTWaiter? 18 | } 19 | 20 | extension _PublisherExpectationBase { 21 | /// :nodoc: 22 | public func _makeWaiter() -> XCTWaiter? { nil } 23 | } 24 | 25 | /// The protocol for publisher expectations. 26 | /// 27 | /// You can build publisher expectations from Recorder returned by the 28 | /// `Publisher.record()` method. 29 | /// 30 | /// For example: 31 | /// 32 | /// // The expectation for all published elements until completion 33 | /// let publisher = ["foo", "bar", "baz"].publisher 34 | /// let recorder = publisher.record() 35 | /// let expectation = recorder.elements 36 | /// 37 | /// When a test grants some time for the expectation to fulfill, use the 38 | /// XCTest `wait(for:timeout:description)` method: 39 | /// 40 | /// // SUCCESS: no timeout, no error 41 | /// func testArrayPublisherPublishesArrayElements() throws { 42 | /// let publisher = ["foo", "bar", "baz"].publisher 43 | /// let recorder = publisher.record() 44 | /// let expectation = recorder.elements 45 | /// let elements = try wait(for: expectation, timeout: 1) 46 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 47 | /// } 48 | /// 49 | /// On the other hand, when the expectation is supposed to be immediately 50 | /// fulfilled, use the PublisherExpectation `get()` method in order to grab the 51 | /// expected value: 52 | /// 53 | /// // SUCCESS: no error 54 | /// func testArrayPublisherSynchronouslyPublishesArrayElements() throws { 55 | /// let publisher = ["foo", "bar", "baz"].publisher 56 | /// let recorder = publisher.record() 57 | /// let elements = try recorder.elements.get() 58 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 59 | /// } 60 | public protocol PublisherExpectation: _PublisherExpectationBase { 61 | /// The type of the expected value. 62 | associatedtype Output 63 | 64 | /// Returns the expected value, or throws an error if the 65 | /// expectation fails. 66 | /// 67 | /// For example: 68 | /// 69 | /// // SUCCESS: no error 70 | /// func testArrayPublisherSynchronouslyPublishesArrayElements() throws { 71 | /// let publisher = ["foo", "bar", "baz"].publisher 72 | /// let recorder = publisher.record() 73 | /// let elements = try recorder.elements.get() 74 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 75 | /// } 76 | func get() throws -> Output 77 | } 78 | 79 | extension XCTestCase { 80 | /// Waits for the publisher expectation to fulfill, and returns the 81 | /// expected value. 82 | /// 83 | /// For example: 84 | /// 85 | /// // SUCCESS: no timeout, no error 86 | /// func testArrayPublisherPublishesArrayElements() throws { 87 | /// let publisher = ["foo", "bar", "baz"].publisher 88 | /// let recorder = publisher.record() 89 | /// let elements = try wait(for: recorder.elements, timeout: 1) 90 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 91 | /// } 92 | /// 93 | /// - parameter publisherExpectation: The publisher expectation. 94 | /// - parameter timeout: The number of seconds within which the expectation 95 | /// must be fulfilled. 96 | /// - parameter description: A string to display in the test log for the 97 | /// expectation, to help diagnose failures. 98 | /// - throws: An error if the expectation fails. 99 | public func wait( 100 | for publisherExpectation: R, 101 | timeout: TimeInterval, 102 | description: String = "") 103 | throws -> R.Output 104 | { 105 | let expectation = self.expectation(description: description) 106 | publisherExpectation._setup(expectation) 107 | if let waiter = publisherExpectation._makeWaiter() { 108 | waiter.wait(for: [expectation], timeout: timeout) 109 | } else { 110 | wait(for: [expectation], timeout: timeout) 111 | } 112 | return try publisherExpectation.get() 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/AvailableElements.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension PublisherExpectations { 4 | /// A publisher expectation which waits for the timeout to expire, or 5 | /// the recorded publisher to complete. 6 | /// 7 | /// When waiting for this expectation, the publisher error is thrown if 8 | /// the publisher fails before the expectation has expired. 9 | /// 10 | /// Otherwise, an array of all elements published before the expectation 11 | /// has expired is returned. 12 | /// 13 | /// Unlike other expectations, `AvailableElements` does not make a test fail 14 | /// on timeout expiration. It just returns the elements published so far. 15 | /// 16 | /// For example: 17 | /// 18 | /// // SUCCESS: no timeout, no error 19 | /// func testTimerPublishesIncreasingDates() throws { 20 | /// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() 21 | /// let recorder = publisher.record() 22 | /// let dates = try wait(for: recorder.availableElements, timeout: ...) 23 | /// XCTAssertEqual(dates.sorted(), dates) 24 | /// } 25 | public struct AvailableElements: PublisherExpectation { 26 | let recorder: Recorder 27 | 28 | public func _makeWaiter() -> XCTWaiter? { Waiter() } 29 | 30 | public func _setup(_ expectation: XCTestExpectation) { 31 | recorder.fulfillOnCompletion(expectation) 32 | } 33 | 34 | /// Returns all elements published so far, or throws an error if the 35 | /// publisher has failed. 36 | public func get() throws -> [Input] { 37 | try recorder.value { (elements, completion, remainingElements, consume) in 38 | if case let .failure(error) = completion { 39 | throw error 40 | } 41 | consume(remainingElements.count) 42 | return elements 43 | } 44 | } 45 | 46 | /// A waiter that waits but never fails 47 | private class Waiter: XCTWaiter, XCTWaiterDelegate { 48 | init() { 49 | super.init(delegate: nil) 50 | delegate = self 51 | } 52 | 53 | func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { } 54 | func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { } 55 | func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { } 56 | func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/Finished.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | // The Finished expectation waits for the publisher to complete, and throws an 4 | // error if and only if the publisher fails with an error. 5 | // 6 | // It is not derived from the Recording expectation, because Finished does not 7 | // throw RecordingError.notCompleted if the publisher does not complete on time. 8 | // It only triggers a timeout test failure. 9 | // 10 | // This allows to write tests for publishers that should not complete: 11 | // 12 | // // SUCCESS: no timeout, no error 13 | // func testPassthroughSubjectDoesNotFinish() throws { 14 | // let publisher = PassthroughSubject() 15 | // let recorder = publisher.record() 16 | // try wait(for: recorder.finished.inverted, timeout: 1) 17 | // } 18 | 19 | extension PublisherExpectations { 20 | /// A publisher expectation which waits for the recorded publisher 21 | /// to complete. 22 | /// 23 | /// When waiting for this expectation, the publisher error is thrown if the 24 | /// publisher fails. 25 | /// 26 | /// For example: 27 | /// 28 | /// // SUCCESS: no timeout, no error 29 | /// func testArrayPublisherFinishesWithoutError() throws { 30 | /// let publisher = ["foo", "bar", "baz"].publisher 31 | /// let recorder = publisher.record() 32 | /// try wait(for: recorder.finished, timeout: 1) 33 | /// } 34 | /// 35 | /// This publisher expectation can be inverted: 36 | /// 37 | /// // SUCCESS: no timeout, no error 38 | /// func testPassthroughSubjectDoesNotFinish() throws { 39 | /// let publisher = PassthroughSubject() 40 | /// let recorder = publisher.record() 41 | /// try wait(for: recorder.finished.inverted, timeout: 1) 42 | /// } 43 | public struct Finished: PublisherExpectation { 44 | let recorder: Recorder 45 | 46 | public func _setup(_ expectation: XCTestExpectation) { 47 | recorder.fulfillOnCompletion(expectation) 48 | } 49 | 50 | /// Returns the expected output, or throws an error if the 51 | /// expectation fails. 52 | /// 53 | /// For example: 54 | /// 55 | /// // SUCCESS: no error 56 | /// func testArrayPublisherSynchronouslyFinishesWithoutError() throws { 57 | /// let publisher = ["foo", "bar", "baz"].publisher 58 | /// let recorder = publisher.record() 59 | /// try recorder.finished.get() 60 | /// } 61 | public func get() throws { 62 | try recorder.value { (_, completion, remainingElements, consume) in 63 | guard let completion = completion else { 64 | consume(remainingElements.count) 65 | return 66 | } 67 | if case let .failure(error) = completion { 68 | throw error 69 | } 70 | } 71 | } 72 | 73 | /// Returns an inverted publisher expectation which waits for a 74 | /// publisher to complete successfully. 75 | /// 76 | /// When waiting for this expectation, an error is thrown if the 77 | /// publisher fails with an error. 78 | /// 79 | /// For example: 80 | /// 81 | /// // SUCCESS: no timeout, no error 82 | /// func testPassthroughSubjectDoesNotFinish() throws { 83 | /// let publisher = PassthroughSubject() 84 | /// let recorder = publisher.record() 85 | /// try wait(for: recorder.finished.inverted, timeout: 1) 86 | /// } 87 | public var inverted: Inverted { 88 | return Inverted(base: self) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/Inverted.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension PublisherExpectations { 4 | /// A publisher expectation that fails if the base expectation is fulfilled. 5 | /// 6 | /// When waiting for this expectation, you receive the same result and 7 | /// eventual error as the base expectation. 8 | /// 9 | /// For example: 10 | /// 11 | /// // SUCCESS: no timeout, no error 12 | /// func testPassthroughSubjectDoesNotFinish() throws { 13 | /// let publisher = PassthroughSubject() 14 | /// let recorder = publisher.record() 15 | /// try wait(for: recorder.finished.inverted, timeout: 1) 16 | /// } 17 | public struct Inverted: PublisherExpectation { 18 | let base: Base 19 | 20 | public func _setup(_ expectation: XCTestExpectation) { 21 | base._setup(expectation) 22 | expectation.isInverted.toggle() 23 | } 24 | 25 | public func get() throws -> Base.Output { 26 | try base.get() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/Map.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension PublisherExpectations { 4 | /// A publisher expectation that transforms the value of a base expectation. 5 | /// 6 | /// This expectation has no public initializer. 7 | public struct Map: PublisherExpectation { 8 | let base: Base 9 | let transform: (Base.Output) throws -> Output 10 | 11 | public func _setup(_ expectation: XCTestExpectation) { 12 | base._setup(expectation) 13 | } 14 | 15 | public func get() throws -> Output { 16 | try transform(base.get()) 17 | } 18 | } 19 | } 20 | 21 | extension PublisherExpectation { 22 | /// Returns a publisher expectation that transforms the value of the 23 | /// base expectation. 24 | func map(_ transform: @escaping (Output) throws -> T) -> PublisherExpectations.Map { 25 | PublisherExpectations.Map(base: self, transform: transform) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/Next.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension PublisherExpectations { 4 | /// A publisher expectation which waits for the recorded publisher to emit 5 | /// `count` elements, or to complete. 6 | /// 7 | /// When waiting for this expectation, a `RecordingError.notEnoughElements` 8 | /// is thrown if the publisher does not publish `count` elements after last 9 | /// waited expectation. The publisher error is thrown if the publisher fails 10 | /// before publishing the next `count` elements. 11 | /// 12 | /// Otherwise, an array of exactly `count` elements is returned. 13 | /// 14 | /// For example: 15 | /// 16 | /// // SUCCESS: no timeout, no error 17 | /// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { 18 | /// let publisher = ["foo", "bar", "baz"].publisher 19 | /// let recorder = publisher.record() 20 | /// 21 | /// var elements = try wait(for: recorder.next(2), timeout: 1) 22 | /// XCTAssertEqual(elements, ["foo", "bar"]) 23 | /// 24 | /// elements = try wait(for: recorder.next(1), timeout: 1) 25 | /// XCTAssertEqual(elements, ["baz"]) 26 | /// } 27 | public struct Next: PublisherExpectation { 28 | let recorder: Recorder 29 | let count: Int 30 | 31 | init(recorder: Recorder, count: Int) { 32 | precondition(count >= 0, "Can't take a prefix of negative length") 33 | self.recorder = recorder 34 | self.count = count 35 | } 36 | 37 | public func _setup(_ expectation: XCTestExpectation) { 38 | if count == 0 { 39 | // Such an expectation is immediately fulfilled, by essence. 40 | expectation.expectedFulfillmentCount = 1 41 | expectation.fulfill() 42 | } else { 43 | expectation.expectedFulfillmentCount = count 44 | recorder.fulfillOnInput(expectation, includingConsumed: false) 45 | } 46 | } 47 | 48 | /// Returns the expected output, or throws an error if the 49 | /// expectation fails. 50 | /// 51 | /// For example: 52 | /// 53 | /// // SUCCESS: no error 54 | /// func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws { 55 | /// let publisher = ["foo", "bar", "baz"].publisher 56 | /// let recorder = publisher.record() 57 | /// 58 | /// var elements = try recorder.next(2).get() 59 | /// XCTAssertEqual(elements, ["foo", "bar"]) 60 | /// 61 | /// elements = try recorder.next(1).get() 62 | /// XCTAssertEqual(elements, ["baz"]) 63 | /// } 64 | public func get() throws -> [Input] { 65 | try recorder.value { (_, completion, remainingElements, consume) in 66 | if remainingElements.count >= count { 67 | consume(count) 68 | return Array(remainingElements.prefix(count)) 69 | } 70 | if case let .failure(error) = completion { 71 | throw error 72 | } else { 73 | throw RecordingError.notEnoughElements 74 | } 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/NextOne.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension PublisherExpectations { 4 | /// A publisher expectation which waits for the recorded publisher to emit 5 | /// one element, or to complete. 6 | /// 7 | /// When waiting for this expectation, a `RecordingError.notEnoughElements` 8 | /// is thrown if the publisher does not publish one element after last 9 | /// waited expectation. The publisher error is thrown if the publisher fails 10 | /// before publishing the next element. 11 | /// 12 | /// Otherwise, the next published element is returned. 13 | /// 14 | /// For example: 15 | /// 16 | /// // SUCCESS: no timeout, no error 17 | /// func testArrayOfTwoElementsPublishesElementsInOrder() throws { 18 | /// let publisher = ["foo", "bar"].publisher 19 | /// let recorder = publisher.record() 20 | /// 21 | /// var element = try wait(for: recorder.next(), timeout: 1) 22 | /// XCTAssertEqual(element, "foo") 23 | /// 24 | /// element = try wait(for: recorder.next(), timeout: 1) 25 | /// XCTAssertEqual(element, "bar") 26 | /// } 27 | public struct NextOne: PublisherExpectation { 28 | let recorder: Recorder 29 | 30 | public func _setup(_ expectation: XCTestExpectation) { 31 | recorder.fulfillOnInput(expectation, includingConsumed: false) 32 | } 33 | 34 | /// Returns the expected output, or throws an error if the 35 | /// expectation fails. 36 | /// 37 | /// For example: 38 | /// 39 | /// // SUCCESS: no error 40 | /// func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws { 41 | /// let publisher = ["foo", "bar"].publisher 42 | /// let recorder = publisher.record() 43 | /// 44 | /// var element = try recorder.next().get() 45 | /// XCTAssertEqual(element, "foo") 46 | /// 47 | /// element = try recorder.next().get() 48 | /// XCTAssertEqual(element, "bar") 49 | /// } 50 | public func get() throws -> Input { 51 | try recorder.value { (_, completion, remainingElements, consume) in 52 | if let next = remainingElements.first { 53 | consume(1) 54 | return next 55 | } 56 | if case let .failure(error) = completion { 57 | throw error 58 | } else { 59 | throw RecordingError.notEnoughElements 60 | } 61 | } 62 | } 63 | 64 | /// Returns an inverted publisher expectation which waits for the 65 | /// recorded publisher to emit one element, or to complete. 66 | /// 67 | /// When waiting for this expectation, a RecordingError is thrown if the 68 | /// publisher does not publish one element after last waited 69 | /// expectation. The publisher error is thrown if the publisher fails 70 | /// before publishing one element. 71 | /// 72 | /// For example: 73 | /// 74 | /// // SUCCESS: no timeout, no error 75 | /// func testPassthroughSubjectDoesNotPublishAnyElement() throws { 76 | /// let publisher = PassthroughSubject() 77 | /// let recorder = publisher.record() 78 | /// try wait(for: recorder.next().inverted, timeout: 1) 79 | /// } 80 | public var inverted: NextOneInverted { 81 | return NextOneInverted(recorder: recorder) 82 | } 83 | } 84 | 85 | /// An inverted publisher expectation which waits for the recorded publisher 86 | /// to emit one element, or to complete. 87 | /// 88 | /// When waiting for this expectation, a RecordingError is thrown if the 89 | /// publisher does not publish one element after last waited expectation. 90 | /// The publisher error is thrown if the publisher fails before 91 | /// publishing one element. 92 | /// 93 | /// For example: 94 | /// 95 | /// // SUCCESS: no timeout, no error 96 | /// func testPassthroughSubjectDoesNotPublishAnyElement() throws { 97 | /// let publisher = PassthroughSubject() 98 | /// let recorder = publisher.record() 99 | /// try wait(for: recorder.next().inverted, timeout: 1) 100 | /// } 101 | public struct NextOneInverted: PublisherExpectation { 102 | let recorder: Recorder 103 | 104 | public func _setup(_ expectation: XCTestExpectation) { 105 | expectation.isInverted = true 106 | recorder.fulfillOnInput(expectation, includingConsumed: false) 107 | } 108 | 109 | public func get() throws { 110 | try recorder.value { (_, completion, remainingElements, consume) in 111 | if remainingElements.isEmpty == false { 112 | return 113 | } 114 | if case let .failure(error) = completion { 115 | throw error 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/Prefix.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension PublisherExpectations { 4 | /// A publisher expectation which waits for the recorded publisher to emit 5 | /// `maxLength` elements, or to complete. 6 | /// 7 | /// When waiting for this expectation, the publisher error is thrown if the 8 | /// publisher fails before `maxLength` elements are published. 9 | /// 10 | /// Otherwise, an array of received elements is returned, containing at 11 | /// most `maxLength` elements, or less if the publisher completes early. 12 | /// 13 | /// For example: 14 | /// 15 | /// // SUCCESS: no timeout, no error 16 | /// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { 17 | /// let publisher = ["foo", "bar", "baz"].publisher 18 | /// let recorder = publisher.record() 19 | /// let elements = try wait(for: recorder.prefix(2), timeout: 1) 20 | /// XCTAssertEqual(elements, ["foo", "bar"]) 21 | /// } 22 | /// 23 | /// This publisher expectation can be inverted: 24 | /// 25 | /// // SUCCESS: no timeout, no error 26 | /// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { 27 | /// let publisher = PassthroughSubject() 28 | /// let recorder = publisher.record() 29 | /// publisher.send("foo") 30 | /// publisher.send("bar") 31 | /// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1) 32 | /// XCTAssertEqual(elements, ["foo", "bar"]) 33 | /// } 34 | public struct Prefix: PublisherExpectation { 35 | let recorder: Recorder 36 | let maxLength: Int 37 | 38 | init(recorder: Recorder, maxLength: Int) { 39 | precondition(maxLength >= 0, "Can't take a prefix of negative length") 40 | self.recorder = recorder 41 | self.maxLength = maxLength 42 | } 43 | 44 | public func _setup(_ expectation: XCTestExpectation) { 45 | if maxLength == 0 { 46 | // Such an expectation is immediately fulfilled, by essence. 47 | expectation.expectedFulfillmentCount = 1 48 | expectation.fulfill() 49 | } else { 50 | expectation.expectedFulfillmentCount = maxLength 51 | recorder.fulfillOnInput(expectation, includingConsumed: true) 52 | } 53 | } 54 | 55 | /// Returns the expected output, or throws an error if the 56 | /// expectation fails. 57 | /// 58 | /// For example: 59 | /// 60 | /// // SUCCESS: no error 61 | /// func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws { 62 | /// let publisher = ["foo", "bar", "baz"].publisher 63 | /// let recorder = publisher.record() 64 | /// let elements = try recorder.prefix(2).get() 65 | /// XCTAssertEqual(elements, ["foo", "bar"]) 66 | /// } 67 | public func get() throws -> [Input] { 68 | try recorder.value { (elements, completion, remainingElements, consume) in 69 | if elements.count >= maxLength { 70 | let extraCount = max(maxLength + remainingElements.count - elements.count, 0) 71 | consume(extraCount) 72 | return Array(elements.prefix(maxLength)) 73 | } 74 | if case let .failure(error) = completion { 75 | throw error 76 | } 77 | consume(remainingElements.count) 78 | return elements 79 | } 80 | } 81 | 82 | /// Returns an inverted publisher expectation which waits for a 83 | /// publisher to emit `maxLength` elements, or to complete. 84 | /// 85 | /// When waiting for this expectation, the publisher error is thrown 86 | /// if the publisher fails before `maxLength` elements are published. 87 | /// 88 | /// Otherwise, an array of received elements is returned, containing at 89 | /// most `maxLength` elements, or less if the publisher completes early. 90 | /// 91 | /// For example: 92 | /// 93 | /// // SUCCESS: no timeout, no error 94 | /// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { 95 | /// let publisher = PassthroughSubject() 96 | /// let recorder = publisher.record() 97 | /// publisher.send("foo") 98 | /// publisher.send("bar") 99 | /// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1) 100 | /// XCTAssertEqual(elements, ["foo", "bar"]) 101 | /// } 102 | public var inverted: Inverted { 103 | return Inverted(base: self) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/PublisherExpectations/Recording.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import XCTest 3 | 4 | extension PublisherExpectations { 5 | /// A publisher expectation which waits for the recorded publisher 6 | /// to complete. 7 | /// 8 | /// When waiting for this expectation, a RecordingError.notCompleted is 9 | /// thrown if the publisher does not complete on time. 10 | /// 11 | /// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording) 12 | /// is returned. 13 | /// 14 | /// For example: 15 | /// 16 | /// // SUCCESS: no timeout, no error 17 | /// func testArrayPublisherRecording() throws { 18 | /// let publisher = ["foo", "bar", "baz"].publisher 19 | /// let recorder = publisher.record() 20 | /// let recording = try wait(for: recorder.recording, timeout: 1) 21 | /// XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 22 | /// if case let .failure(error) = recording.completion { 23 | /// XCTFail("Unexpected error \(error)") 24 | /// } 25 | /// } 26 | public struct Recording: PublisherExpectation { 27 | let recorder: Recorder 28 | 29 | public func _setup(_ expectation: XCTestExpectation) { 30 | recorder.fulfillOnCompletion(expectation) 31 | } 32 | 33 | /// Returns the expected output, or throws an error if the 34 | /// expectation fails. 35 | /// 36 | /// For example: 37 | /// 38 | /// // SUCCESS: no error 39 | /// func testArrayPublisherSynchronousRecording() throws { 40 | /// let publisher = ["foo", "bar", "baz"].publisher 41 | /// let recorder = publisher.record() 42 | /// let recording = try recorder.recording.get() 43 | /// XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 44 | /// if case let .failure(error) = recording.completion { 45 | /// XCTFail("Unexpected error \(error)") 46 | /// } 47 | /// } 48 | public func get() throws -> Record.Recording { 49 | try recorder.value { (elements, completion, remainingElements, consume) in 50 | if let completion = completion { 51 | consume(remainingElements.count) 52 | return Record.Recording(output: elements, completion: completion) 53 | } else { 54 | throw RecordingError.notCompleted 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/CombineExpectations/Recorder.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import XCTest 3 | 4 | /// A Combine subscriber which records all events published by a publisher. 5 | /// 6 | /// You create a Recorder with the `Publisher.record()` method: 7 | /// 8 | /// let publisher = ["foo", "bar", "baz"].publisher 9 | /// let recorder = publisher.record() 10 | /// 11 | /// You can build publisher expectations from the Recorder. For example: 12 | /// 13 | /// let elements = try wait(for: recorder.elements, timeout: 1) 14 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 15 | public class Recorder: Subscriber { 16 | public typealias Input = Input 17 | public typealias Failure = Failure 18 | 19 | private enum RecorderExpectation { 20 | case onInput(XCTestExpectation, remainingCount: Int) 21 | case onCompletion(XCTestExpectation) 22 | 23 | var expectation: XCTestExpectation { 24 | switch self { 25 | case let .onCompletion(expectation): 26 | return expectation 27 | case let .onInput(expectation, remainingCount: _): 28 | return expectation 29 | } 30 | } 31 | } 32 | 33 | /// The recorder state 34 | private enum State { 35 | /// Publisher is not subscribed yet. The recorder may have an 36 | /// expectation to fulfill. 37 | case waitingForSubscription(RecorderExpectation?) 38 | 39 | /// Publisher is subscribed. The recorder may have an expectation to 40 | /// fulfill. It keeps track of all published elements. 41 | case subscribed(Subscription, RecorderExpectation?, [Input]) 42 | 43 | /// Publisher is completed. The recorder keeps track of all published 44 | /// elements and completion. 45 | case completed([Input], Subscribers.Completion) 46 | 47 | var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion?) { 48 | switch self { 49 | case .waitingForSubscription: 50 | return (elements: [], completion: nil) 51 | case let .subscribed(_, _, elements): 52 | return (elements: elements, completion: nil) 53 | case let .completed(elements, completion): 54 | return (elements: elements, completion: completion) 55 | } 56 | } 57 | 58 | var recorderExpectation: RecorderExpectation? { 59 | switch self { 60 | case let .waitingForSubscription(exp), let .subscribed(_, exp, _): 61 | return exp 62 | case .completed: 63 | return nil 64 | } 65 | } 66 | } 67 | 68 | private let lock = NSLock() 69 | private var state = State.waitingForSubscription(nil) 70 | private var consumedCount = 0 71 | 72 | /// The elements and completion recorded so far. 73 | var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion?) { 74 | synchronized { 75 | state.elementsAndCompletion 76 | } 77 | } 78 | 79 | /// Use Publisher.record() 80 | fileprivate init() { } 81 | 82 | deinit { 83 | if case let .subscribed(subscription, _, _) = state { 84 | subscription.cancel() 85 | } 86 | } 87 | 88 | private func synchronized(_ execute: () throws -> T) rethrows -> T { 89 | lock.lock() 90 | defer { lock.unlock() } 91 | return try execute() 92 | } 93 | 94 | // MARK: - PublisherExpectation API 95 | 96 | /// Registers the expectation so that it gets fulfilled when publisher 97 | /// publishes elements or completes. 98 | /// 99 | /// - parameter expectation: An XCTestExpectation. 100 | /// - parameter includingConsumed: This flag controls how elements that were 101 | /// already published at the time this method is called fulfill the 102 | /// expectation. If true, all published elements fulfill the expectation. 103 | /// If false, only published elements that are not consumed yet fulfill 104 | /// the expectation. For example, the Prefix expectation uses true, but 105 | /// the NextOne expectation uses false. 106 | func fulfillOnInput(_ expectation: XCTestExpectation, includingConsumed: Bool) { 107 | synchronized { 108 | preconditionCanFulfillExpectation() 109 | 110 | let expectedFulfillmentCount = expectation.expectedFulfillmentCount 111 | 112 | switch state { 113 | case .waitingForSubscription: 114 | let exp = RecorderExpectation.onInput(expectation, remainingCount: expectedFulfillmentCount) 115 | state = .waitingForSubscription(exp) 116 | 117 | case let .subscribed(subscription, _, elements): 118 | let maxFulfillmentCount = includingConsumed 119 | ? elements.count 120 | : elements.count - consumedCount 121 | let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount) 122 | expectation.fulfill(count: fulfillmentCount) 123 | 124 | let remainingCount = expectedFulfillmentCount - fulfillmentCount 125 | if remainingCount > 0 { 126 | let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount) 127 | state = .subscribed(subscription, exp, elements) 128 | } 129 | 130 | case .completed: 131 | expectation.fulfill(count: expectedFulfillmentCount) 132 | } 133 | } 134 | } 135 | 136 | /// Registers the expectation so that it gets fulfilled when 137 | /// publisher completes. 138 | func fulfillOnCompletion(_ expectation: XCTestExpectation) { 139 | synchronized { 140 | preconditionCanFulfillExpectation() 141 | 142 | switch state { 143 | case .waitingForSubscription: 144 | let exp = RecorderExpectation.onCompletion(expectation) 145 | state = .waitingForSubscription(exp) 146 | 147 | case let .subscribed(subscription, _, elements): 148 | let exp = RecorderExpectation.onCompletion(expectation) 149 | state = .subscribed(subscription, exp, elements) 150 | 151 | case .completed: 152 | expectation.fulfill() 153 | } 154 | } 155 | } 156 | 157 | /// Returns a value based on the recorded state of the publisher. 158 | /// 159 | /// - parameter value: A function which returns the value, given the 160 | /// recorded state of the publisher. 161 | /// - parameter elements: All recorded elements. 162 | /// - parameter completion: The eventual publisher completion. 163 | /// - parameter remainingElements: The elements that were not consumed yet. 164 | /// - parameter consume: A function which consumes elements. 165 | /// - parameter count: The number of consumed elements. 166 | /// - returns: The value 167 | func value(_ value: ( 168 | _ elements: [Input], 169 | _ completion: Subscribers.Completion?, 170 | _ remainingElements: ArraySlice, 171 | _ consume: (_ count: Int) -> ()) throws -> T) 172 | rethrows -> T 173 | { 174 | try synchronized { 175 | let (elements, completion) = state.elementsAndCompletion 176 | let remainingElements = elements[consumedCount...] 177 | return try value(elements, completion, remainingElements, { count in 178 | precondition(count >= 0) 179 | precondition(count <= remainingElements.count) 180 | consumedCount += count 181 | }) 182 | } 183 | } 184 | 185 | /// Checks that recorder can fulfill an expectation. 186 | /// 187 | /// The reason this method exists is that a recorder can fulfill a single 188 | /// expectation at a given time. It is a programmer error to wait for two 189 | /// expectations concurrently. 190 | /// 191 | /// This method MUST be called within a synchronized block. 192 | private func preconditionCanFulfillExpectation() { 193 | if let exp = state.recorderExpectation { 194 | // We are already waiting for an expectation! Is it a programmer 195 | // error? Recorder drops references to non-inverted expectations 196 | // when they are fulfilled. But inverted expectations are not 197 | // fulfilled, and thus not dropped. We can't quite know if an 198 | // inverted expectations has expired yet, so just let it go. 199 | precondition(exp.expectation.isInverted, "Already waiting for an expectation") 200 | } 201 | } 202 | 203 | // MARK: - Subscriber 204 | 205 | public func receive(subscription: Subscription) { 206 | synchronized { 207 | switch state { 208 | case let .waitingForSubscription(exp): 209 | state = .subscribed(subscription, exp, []) 210 | default: 211 | XCTFail("Publisher recorder is already subscribed") 212 | } 213 | } 214 | subscription.request(.unlimited) 215 | } 216 | 217 | public func receive(_ input: Input) -> Subscribers.Demand { 218 | return synchronized { 219 | switch state { 220 | case let .subscribed(subscription, exp, elements): 221 | var elements = elements 222 | elements.append(input) 223 | 224 | if case let .onInput(expectation, remainingCount: remainingCount) = exp { 225 | assert(remainingCount > 0) 226 | expectation.fulfill() 227 | if remainingCount > 1 { 228 | let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount - 1) 229 | state = .subscribed(subscription, exp, elements) 230 | } else { 231 | state = .subscribed(subscription, nil, elements) 232 | } 233 | } else { 234 | state = .subscribed(subscription, exp, elements) 235 | } 236 | 237 | return .unlimited 238 | 239 | case .waitingForSubscription: 240 | XCTFail("Publisher recorder got unexpected input before subscription: \(String(reflecting: input))") 241 | return .none 242 | 243 | case .completed: 244 | XCTFail("Publisher recorder got unexpected input after completion: \(String(reflecting: input))") 245 | return .none 246 | } 247 | } 248 | } 249 | 250 | public func receive(completion: Subscribers.Completion) { 251 | synchronized { 252 | switch state { 253 | case let .subscribed(_, exp, elements): 254 | if let exp = exp { 255 | switch exp { 256 | case let .onCompletion(expectation): 257 | expectation.fulfill() 258 | case let .onInput(expectation, remainingCount: remainingCount): 259 | expectation.fulfill(count: remainingCount) 260 | } 261 | } 262 | state = .completed(elements, completion) 263 | 264 | case .waitingForSubscription: 265 | XCTFail("Publisher recorder got unexpected completion before subscription: \(String(describing: completion))") 266 | 267 | case .completed: 268 | XCTFail("Publisher recorder got unexpected completion after completion: \(String(describing: completion))") 269 | } 270 | } 271 | } 272 | } 273 | 274 | // MARK: - Publisher Expectations 275 | 276 | extension PublisherExpectations { 277 | /// The type of the publisher expectation returned by `Recorder.completion`. 278 | public typealias Completion = Map, Subscribers.Completion> 279 | 280 | /// The type of the publisher expectation returned by `Recorder.elements`. 281 | public typealias Elements = Map, [Input]> 282 | 283 | /// The type of the publisher expectation returned by `Recorder.last`. 284 | public typealias Last = Map, Input?> 285 | 286 | /// The type of the publisher expectation returned by `Recorder.single`. 287 | public typealias Single = Map, Input> 288 | } 289 | 290 | extension Recorder { 291 | /// Returns a publisher expectation which waits for the timeout to expire, 292 | /// or the recorded publisher to complete. 293 | /// 294 | /// When waiting for this expectation, the publisher error is thrown if 295 | /// the publisher fails before the expectation has expired. 296 | /// 297 | /// Otherwise, an array of all elements published before the expectation 298 | /// has expired is returned. 299 | /// 300 | /// Unlike other expectations, `availableElements` does not make a test fail 301 | /// on timeout expiration. It just returns the elements published so far. 302 | /// 303 | /// For example: 304 | /// 305 | /// // SUCCESS: no timeout, no error 306 | /// func testTimerPublishesIncreasingDates() throws { 307 | /// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() 308 | /// let recorder = publisher.record() 309 | /// let dates = try wait(for: recorder.availableElements, timeout: ...) 310 | /// XCTAssertEqual(dates.sorted(), dates) 311 | /// } 312 | public var availableElements: PublisherExpectations.AvailableElements { 313 | PublisherExpectations.AvailableElements(recorder: self) 314 | } 315 | 316 | /// Returns a publisher expectation which waits for the recorded publisher 317 | /// to complete. 318 | /// 319 | /// When waiting for this expectation, a RecordingError.notCompleted is 320 | /// thrown if the publisher does not complete on time. 321 | /// 322 | /// Otherwise, a [Subscribers.Completion](https://developer.apple.com/documentation/combine/subscribers/completion) 323 | /// is returned. 324 | /// 325 | /// For example: 326 | /// 327 | /// // SUCCESS: no timeout, no error 328 | /// func testArrayPublisherCompletesWithSuccess() throws { 329 | /// let publisher = ["foo", "bar", "baz"].publisher 330 | /// let recorder = publisher.record() 331 | /// let completion = try wait(for: recorder.completion, timeout: 1) 332 | /// if case let .failure(error) = completion { 333 | /// XCTFail("Unexpected error \(error)") 334 | /// } 335 | /// } 336 | public var completion: PublisherExpectations.Completion { 337 | recording.map { $0.completion } 338 | } 339 | 340 | /// Returns a publisher expectation which waits for the recorded publisher 341 | /// to complete. 342 | /// 343 | /// When waiting for this expectation, a RecordingError.notCompleted is 344 | /// thrown if the publisher does not complete on time, and the publisher 345 | /// error is thrown if the publisher fails. 346 | /// 347 | /// Otherwise, an array of published elements is returned. 348 | /// 349 | /// For example: 350 | /// 351 | /// // SUCCESS: no timeout, no error 352 | /// func testArrayPublisherPublishesArrayElements() throws { 353 | /// let publisher = ["foo", "bar", "baz"].publisher 354 | /// let recorder = publisher.record() 355 | /// let elements = try wait(for: recorder.elements, timeout: 1) 356 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 357 | /// } 358 | public var elements: PublisherExpectations.Elements { 359 | recording.map { recording in 360 | if case let .failure(error) = recording.completion { 361 | throw error 362 | } 363 | return recording.output 364 | } 365 | } 366 | 367 | /// Returns a publisher expectation which waits for the recorded publisher 368 | /// to complete. 369 | /// 370 | /// When waiting for this expectation, the publisher error is thrown if the 371 | /// publisher fails. 372 | /// 373 | /// For example: 374 | /// 375 | /// // SUCCESS: no timeout, no error 376 | /// func testArrayPublisherFinishesWithoutError() throws { 377 | /// let publisher = ["foo", "bar", "baz"].publisher 378 | /// let recorder = publisher.record() 379 | /// try wait(for: recorder.finished, timeout: 1) 380 | /// } 381 | /// 382 | /// This publisher expectation can be inverted: 383 | /// 384 | /// // SUCCESS: no timeout, no error 385 | /// func testPassthroughSubjectDoesNotFinish() throws { 386 | /// let publisher = PassthroughSubject() 387 | /// let recorder = publisher.record() 388 | /// try wait(for: recorder.finished.inverted, timeout: 1) 389 | /// } 390 | public var finished: PublisherExpectations.Finished { 391 | PublisherExpectations.Finished(recorder: self) 392 | } 393 | 394 | /// Returns a publisher expectation which waits for the recorded publisher 395 | /// to complete. 396 | /// 397 | /// When waiting for this expectation, a RecordingError.notCompleted is 398 | /// thrown if the publisher does not complete on time, and the publisher 399 | /// error is thrown if the publisher fails. 400 | /// 401 | /// Otherwise, the last published element is returned, or nil if the publisher 402 | /// completes before it publishes any element. 403 | /// 404 | /// For example: 405 | /// 406 | /// // SUCCESS: no timeout, no error 407 | /// func testArrayPublisherPublishesLastElementLast() throws { 408 | /// let publisher = ["foo", "bar", "baz"].publisher 409 | /// let recorder = publisher.record() 410 | /// if let element = try wait(for: recorder.last, timeout: 1) { 411 | /// XCTAssertEqual(element, "baz") 412 | /// } else { 413 | /// XCTFail("Expected one element") 414 | /// } 415 | /// } 416 | public var last: PublisherExpectations.Last { 417 | elements.map { $0.last } 418 | } 419 | 420 | /// Returns a publisher expectation which waits for the recorded publisher 421 | /// to emit one element, or to complete. 422 | /// 423 | /// When waiting for this expectation, a `RecordingError.notEnoughElements` 424 | /// is thrown if the publisher does not publish one element after last 425 | /// waited expectation. The publisher error is thrown if the publisher fails 426 | /// before publishing the next element. 427 | /// 428 | /// Otherwise, the next published element is returned. 429 | /// 430 | /// For example: 431 | /// 432 | /// // SUCCESS: no timeout, no error 433 | /// func testArrayOfTwoElementsPublishesElementsInOrder() throws { 434 | /// let publisher = ["foo", "bar"].publisher 435 | /// let recorder = publisher.record() 436 | /// 437 | /// var element = try wait(for: recorder.next(), timeout: 1) 438 | /// XCTAssertEqual(element, "foo") 439 | /// 440 | /// element = try wait(for: recorder.next(), timeout: 1) 441 | /// XCTAssertEqual(element, "bar") 442 | /// } 443 | public func next() -> PublisherExpectations.NextOne { 444 | PublisherExpectations.NextOne(recorder: self) 445 | } 446 | 447 | /// Returns a publisher expectation which waits for the recorded publisher 448 | /// to emit `count` elements, or to complete. 449 | /// 450 | /// When waiting for this expectation, a `RecordingError.notEnoughElements` 451 | /// is thrown if the publisher does not publish `count` elements after last 452 | /// waited expectation. The publisher error is thrown if the publisher fails 453 | /// before publishing the next `count` elements. 454 | /// 455 | /// Otherwise, an array of exactly `count` elements is returned. 456 | /// 457 | /// For example: 458 | /// 459 | /// // SUCCESS: no timeout, no error 460 | /// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { 461 | /// let publisher = ["foo", "bar", "baz"].publisher 462 | /// let recorder = publisher.record() 463 | /// 464 | /// var elements = try wait(for: recorder.next(2), timeout: 1) 465 | /// XCTAssertEqual(elements, ["foo", "bar"]) 466 | /// 467 | /// elements = try wait(for: recorder.next(1), timeout: 1) 468 | /// XCTAssertEqual(elements, ["baz"]) 469 | /// } 470 | /// 471 | /// - parameter count: The number of elements. 472 | public func next(_ count: Int) -> PublisherExpectations.Next { 473 | PublisherExpectations.Next(recorder: self, count: count) 474 | } 475 | 476 | /// Returns a publisher expectation which waits for the recorded publisher 477 | /// to emit `maxLength` elements, or to complete. 478 | /// 479 | /// When waiting for this expectation, the publisher error is thrown if the 480 | /// publisher fails before `maxLength` elements are published. 481 | /// 482 | /// Otherwise, an array of received elements is returned, containing at 483 | /// most `maxLength` elements, or less if the publisher completes early. 484 | /// 485 | /// For example: 486 | /// 487 | /// // SUCCESS: no timeout, no error 488 | /// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { 489 | /// let publisher = ["foo", "bar", "baz"].publisher 490 | /// let recorder = publisher.record() 491 | /// let elements = try wait(for: recorder.prefix(2), timeout: 1) 492 | /// XCTAssertEqual(elements, ["foo", "bar"]) 493 | /// } 494 | /// 495 | /// This publisher expectation can be inverted: 496 | /// 497 | /// // SUCCESS: no timeout, no error 498 | /// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { 499 | /// let publisher = PassthroughSubject() 500 | /// let recorder = publisher.record() 501 | /// publisher.send("foo") 502 | /// publisher.send("bar") 503 | /// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1) 504 | /// XCTAssertEqual(elements, ["foo", "bar"]) 505 | /// } 506 | /// 507 | /// - parameter maxLength: The maximum number of elements. 508 | public func prefix(_ maxLength: Int) -> PublisherExpectations.Prefix { 509 | PublisherExpectations.Prefix(recorder: self, maxLength: maxLength) 510 | } 511 | 512 | /// Returns a publisher expectation which waits for the recorded publisher 513 | /// to complete. 514 | /// 515 | /// When waiting for this expectation, a RecordingError.notCompleted is 516 | /// thrown if the publisher does not complete on time. 517 | /// 518 | /// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording) 519 | /// is returned. 520 | /// 521 | /// For example: 522 | /// 523 | /// // SUCCESS: no timeout, no error 524 | /// func testArrayPublisherRecording() throws { 525 | /// let publisher = ["foo", "bar", "baz"].publisher 526 | /// let recorder = publisher.record() 527 | /// let recording = try wait(for: recorder.recording, timeout: 1) 528 | /// XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 529 | /// if case let .failure(error) = recording.completion { 530 | /// XCTFail("Unexpected error \(error)") 531 | /// } 532 | /// } 533 | public var recording: PublisherExpectations.Recording { 534 | PublisherExpectations.Recording(recorder: self) 535 | } 536 | 537 | /// Returns a publisher expectation which waits for the recorded publisher 538 | /// to complete. 539 | /// 540 | /// When waiting for this expectation, a RecordingError is thrown if the 541 | /// publisher does not complete on time, or does not publish exactly one 542 | /// element before it completes. The publisher error is thrown if the 543 | /// publisher fails. 544 | /// 545 | /// Otherwise, the single published element is returned. 546 | /// 547 | /// For example: 548 | /// 549 | /// // SUCCESS: no timeout, no error 550 | /// func testJustPublishesExactlyOneElement() throws { 551 | /// let publisher = Just("foo") 552 | /// let recorder = publisher.record() 553 | /// let element = try wait(for: recorder.single, timeout: 1) 554 | /// XCTAssertEqual(element, "foo") 555 | /// } 556 | public var single: PublisherExpectations.Single { 557 | elements.map { elements in 558 | guard let element = elements.first else { 559 | throw RecordingError.notEnoughElements 560 | } 561 | if elements.count > 1 { 562 | throw RecordingError.tooManyElements 563 | } 564 | return element 565 | } 566 | } 567 | } 568 | 569 | // MARK: - Publisher + Recorder 570 | 571 | extension Publisher { 572 | /// Returns a subscribed Recorder. 573 | /// 574 | /// For example: 575 | /// 576 | /// let publisher = ["foo", "bar", "baz"].publisher 577 | /// let recorder = publisher.record() 578 | /// 579 | /// You can build publisher expectations from the Recorder. For example: 580 | /// 581 | /// let elements = try wait(for: recorder.elements, timeout: 1) 582 | /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) 583 | public func record() -> Recorder { 584 | let recorder = Recorder() 585 | subscribe(recorder) 586 | return recorder 587 | } 588 | } 589 | 590 | // MARK: - Convenience 591 | 592 | extension XCTestExpectation { 593 | fileprivate func fulfill(count: Int) { 594 | for _ in 0..() 37 | let recorder = publisher.record() 38 | _ = try wait(for: recorder.completion, timeout: 0.1) 39 | XCTFail("Expected error") 40 | } catch RecordingError.notCompleted { } 41 | } 42 | } 43 | 44 | // MARK: - Elements 45 | 46 | func testArrayPublisherSynchronouslyPublishesArrayElements() throws { 47 | let publisher = ["foo", "bar", "baz"].publisher 48 | let recorder = publisher.record() 49 | let elements = try recorder.elements.get() 50 | XCTAssertEqual(elements, ["foo", "bar", "baz"]) 51 | } 52 | 53 | // SUCCESS: no timeout, no error 54 | func testArrayPublisherPublishesArrayElements() throws { 55 | let publisher = ["foo", "bar", "baz"].publisher 56 | let recorder = publisher.record() 57 | let elements = try wait(for: recorder.elements, timeout: 0.1) 58 | XCTAssertEqual(elements, ["foo", "bar", "baz"]) 59 | } 60 | 61 | // FAIL: Asynchronous wait failed 62 | // FAIL: Caught error RecordingError.notCompleted 63 | func testElementsTimeout() throws { 64 | try assertFailure("Asynchronous wait failed") { 65 | do { 66 | let publisher = PassthroughSubject() 67 | let recorder = publisher.record() 68 | _ = try wait(for: recorder.elements, timeout: 0.1) 69 | XCTFail("Expected error") 70 | } catch RecordingError.notCompleted { } 71 | } 72 | } 73 | 74 | // FAIL: Caught error MyError 75 | func testElementsSynchronousError() throws { 76 | do { 77 | let publisher = PassthroughSubject() 78 | let recorder = publisher.record() 79 | publisher.send(completion: .failure(MyError())) 80 | _ = try recorder.elements.get() 81 | XCTFail("Expected error") 82 | } catch is MyError { } 83 | } 84 | 85 | // FAIL: Caught error MyError 86 | func testElementsError() throws { 87 | do { 88 | let publisher = PassthroughSubject() 89 | let recorder = publisher.record() 90 | publisher.send(completion: .failure(MyError())) 91 | _ = try wait(for: recorder.elements, timeout: 0.1) 92 | XCTFail("Expected error") 93 | } catch is MyError { } 94 | } 95 | 96 | // MARK: - Finished 97 | 98 | // SUCCESS: no error 99 | func testArrayPublisherSynchronouslyFinishesWithoutError() throws { 100 | let publisher = ["foo", "bar", "baz"].publisher 101 | let recorder = publisher.record() 102 | try recorder.finished.get() 103 | } 104 | 105 | // SUCCESS: no timeout, no error 106 | func testArrayPublisherFinishesWithoutError() throws { 107 | let publisher = ["foo", "bar", "baz"].publisher 108 | let recorder = publisher.record() 109 | try wait(for: recorder.finished, timeout: 0.1) 110 | } 111 | 112 | // FAIL: Asynchronous wait failed 113 | func testFinishedTimeout() throws { 114 | try assertFailure("Asynchronous wait failed") { 115 | let publisher = PassthroughSubject() 116 | let recorder = publisher.record() 117 | try wait(for: recorder.finished, timeout: 0.1) 118 | } 119 | } 120 | 121 | // FAIL: Caught error MyError 122 | func testFinishedError() throws { 123 | do { 124 | let publisher = PassthroughSubject() 125 | let recorder = publisher.record() 126 | publisher.send(completion: .failure(MyError())) 127 | try wait(for: recorder.finished, timeout: 0.1) 128 | XCTFail("Expected error") 129 | } catch is MyError { } 130 | } 131 | 132 | // MARK: - Finished.inverted 133 | 134 | // SUCCESS: no timeout, no error 135 | func testPassthroughSubjectDoesNotFinish() throws { 136 | let publisher = PassthroughSubject() 137 | let recorder = publisher.record() 138 | try wait(for: recorder.finished.inverted, timeout: 0.1) 139 | } 140 | 141 | // FAIL: Fulfilled inverted expectation 142 | // FAIL: Caught error MyError 143 | func testInvertedFinishedError() throws { 144 | try assertFailure("Fulfilled inverted expectation") { 145 | do { 146 | let publisher = PassthroughSubject() 147 | let recorder = publisher.record() 148 | publisher.send(completion: .failure(MyError())) 149 | try wait(for: recorder.finished.inverted, timeout: 0.1) 150 | XCTFail("Expected error") 151 | } catch is MyError { } 152 | } 153 | } 154 | 155 | // MARK: - Last 156 | 157 | // SUCCESS: no error 158 | func testArrayPublisherSynchronouslyPublishesLastElementLast() throws { 159 | let publisher = ["foo", "bar", "baz"].publisher 160 | let recorder = publisher.record() 161 | if let element = try recorder.last.get() { 162 | XCTAssertEqual(element, "baz") 163 | } else { 164 | XCTFail("Expected one element") 165 | } 166 | } 167 | 168 | // SUCCESS: no timeout, no error 169 | func testArrayPublisherPublishesLastElementLast() throws { 170 | let publisher = ["foo", "bar", "baz"].publisher 171 | let recorder = publisher.record() 172 | if let element = try wait(for: recorder.last, timeout: 0.1) { 173 | XCTAssertEqual(element, "baz") 174 | } else { 175 | XCTFail("Expected one element") 176 | } 177 | } 178 | 179 | // FAIL: Asynchronous wait failed 180 | // FAIL: Caught error RecordingError.notCompleted 181 | func testLastTimeout() throws { 182 | try assertFailure("Asynchronous wait failed") { 183 | do { 184 | let publisher = PassthroughSubject() 185 | let recorder = publisher.record() 186 | _ = try wait(for: recorder.last, timeout: 0.1) 187 | XCTFail("Expected error") 188 | } catch RecordingError.notCompleted { } 189 | } 190 | } 191 | 192 | // FAIL: Caught error MyError 193 | func testLastError() throws { 194 | do { 195 | let publisher = PassthroughSubject() 196 | let recorder = publisher.record() 197 | publisher.send(completion: .failure(MyError())) 198 | _ = try wait(for: recorder.last, timeout: 0.1) 199 | XCTFail("Expected error") 200 | } catch is MyError { } 201 | } 202 | 203 | // MARK: - next() 204 | 205 | // SUCCESS: no error 206 | func testPassthroughSubjectSynchronouslyPublishesElements() throws { 207 | let publisher = PassthroughSubject() 208 | let recorder = publisher.record() 209 | 210 | publisher.send("foo") 211 | try XCTAssertEqual(recorder.next().get(), "foo") 212 | 213 | publisher.send("bar") 214 | try XCTAssertEqual(recorder.next().get(), "bar") 215 | } 216 | 217 | // SUCCESS: no error 218 | func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws { 219 | let publisher = ["foo", "bar"].publisher 220 | let recorder = publisher.record() 221 | 222 | var element = try recorder.next().get() 223 | XCTAssertEqual(element, "foo") 224 | 225 | element = try recorder.next().get() 226 | XCTAssertEqual(element, "bar") 227 | } 228 | 229 | // SUCCESS: no timeout, no error 230 | func testArrayOfTwoElementsPublishesElementsInOrder() throws { 231 | let publisher = ["foo", "bar"].publisher 232 | let recorder = publisher.record() 233 | 234 | var element = try wait(for: recorder.next(), timeout: 0.1) 235 | XCTAssertEqual(element, "foo") 236 | 237 | element = try wait(for: recorder.next(), timeout: 0.1) 238 | XCTAssertEqual(element, "bar") 239 | } 240 | 241 | // FAIL: Asynchronous wait failed 242 | // FAIL: Caught error RecordingError.notEnoughElements 243 | func testNextTimeout() throws { 244 | try assertFailure("Asynchronous wait failed") { 245 | do { 246 | let publisher = PassthroughSubject() 247 | let recorder = publisher.record() 248 | _ = try wait(for: recorder.next(), timeout: 0.1) 249 | XCTFail("Expected error") 250 | } catch RecordingError.notEnoughElements { } 251 | } 252 | } 253 | 254 | // FAIL: Caught error MyError 255 | func testNextError() throws { 256 | do { 257 | let publisher = PassthroughSubject() 258 | let recorder = publisher.record() 259 | publisher.send(completion: .failure(MyError())) 260 | _ = try wait(for: recorder.next(), timeout: 0.1) 261 | XCTFail("Expected error") 262 | } catch is MyError { } 263 | } 264 | 265 | // FAIL: Caught error RecordingError.notEnoughElements 266 | func testNextNotEnoughElementsError() throws { 267 | do { 268 | let publisher = PassthroughSubject() 269 | let recorder = publisher.record() 270 | publisher.send(completion: .finished) 271 | _ = try wait(for: recorder.next(), timeout: 0.1) 272 | XCTFail("Expected error") 273 | } catch RecordingError.notEnoughElements { } 274 | } 275 | 276 | // MARK: - next().inverted 277 | 278 | // SUCCESS: no timeout, no error 279 | func testPassthroughSubjectDoesNotPublishAnyElement() throws { 280 | let publisher = PassthroughSubject() 281 | let recorder = publisher.record() 282 | try wait(for: recorder.next().inverted, timeout: 0.1) 283 | } 284 | 285 | // FAIL: Fulfilled inverted expectation 286 | func testInvertedNextTooEarly() throws { 287 | try assertFailure("Fulfilled inverted expectation") { 288 | let publisher = PassthroughSubject() 289 | let recorder = publisher.record() 290 | publisher.send("foo") 291 | try wait(for: recorder.next().inverted, timeout: 0.1) 292 | } 293 | } 294 | 295 | // FAIL: Fulfilled inverted expectation 296 | // FAIL: Caught error MyError 297 | func testInvertedNextError() throws { 298 | try assertFailure("Fulfilled inverted expectation") { 299 | do { 300 | let publisher = PassthroughSubject() 301 | let recorder = publisher.record() 302 | publisher.send(completion: .failure(MyError())) 303 | try wait(for: recorder.next().inverted, timeout: 0.1) 304 | XCTFail("Expected error") 305 | } catch is MyError { } 306 | } 307 | } 308 | 309 | // MARK: - next(count) 310 | 311 | // SUCCESS: no error 312 | func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws { 313 | let publisher = ["foo", "bar", "baz"].publisher 314 | let recorder = publisher.record() 315 | 316 | var elements = try recorder.next(2).get() 317 | XCTAssertEqual(elements, ["foo", "bar"]) 318 | 319 | elements = try recorder.next(1).get() 320 | XCTAssertEqual(elements, ["baz"]) 321 | } 322 | 323 | // SUCCESS: no timeout, no error 324 | func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { 325 | let publisher = ["foo", "bar", "baz"].publisher 326 | let recorder = publisher.record() 327 | 328 | var elements = try wait(for: recorder.next(2), timeout: 0.1) 329 | XCTAssertEqual(elements, ["foo", "bar"]) 330 | 331 | elements = try wait(for: recorder.next(1), timeout: 0.1) 332 | XCTAssertEqual(elements, ["baz"]) 333 | } 334 | 335 | // FAIL: Asynchronous wait failed 336 | // FAIL: Caught error RecordingError.notEnoughElements 337 | func testNextCountTimeout() throws { 338 | try assertFailure("Asynchronous wait failed") { 339 | do { 340 | let publisher = PassthroughSubject() 341 | let recorder = publisher.record() 342 | publisher.send("foo") 343 | _ = try wait(for: recorder.next(2), timeout: 0.1) 344 | XCTFail("Expected error") 345 | } catch RecordingError.notEnoughElements { } 346 | } 347 | } 348 | 349 | // FAIL: Caught error MyError 350 | func testNextCountError() throws { 351 | do { 352 | let publisher = PassthroughSubject() 353 | let recorder = publisher.record() 354 | publisher.send("foo") 355 | publisher.send(completion: .failure(MyError())) 356 | _ = try wait(for: recorder.next(2), timeout: 0.1) 357 | XCTFail("Expected error") 358 | } catch is MyError { } 359 | } 360 | 361 | // FAIL: Caught error RecordingError.notEnoughElements 362 | func testNextCountNotEnoughElementsError() throws { 363 | do { 364 | let publisher = PassthroughSubject() 365 | let recorder = publisher.record() 366 | publisher.send("foo") 367 | publisher.send(completion: .finished) 368 | _ = try wait(for: recorder.next(2), timeout: 0.1) 369 | XCTFail("Expected error") 370 | } catch RecordingError.notEnoughElements { } 371 | } 372 | 373 | // MARK: - Prefix 374 | 375 | // SUCCESS: no error 376 | func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws { 377 | let publisher = ["foo", "bar", "baz"].publisher 378 | let recorder = publisher.record() 379 | let elements = try recorder.prefix(2).get() 380 | XCTAssertEqual(elements, ["foo", "bar"]) 381 | } 382 | 383 | // SUCCESS: no timeout, no error 384 | func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { 385 | let publisher = ["foo", "bar", "baz"].publisher 386 | let recorder = publisher.record() 387 | let elements = try wait(for: recorder.prefix(2), timeout: 0.1) 388 | XCTAssertEqual(elements, ["foo", "bar"]) 389 | } 390 | 391 | // FAIL: Asynchronous wait failed 392 | func testPrefixTimeout() throws { 393 | try assertFailure("Asynchronous wait failed") { 394 | let publisher = PassthroughSubject() 395 | let recorder = publisher.record() 396 | publisher.send("foo") 397 | _ = try wait(for: recorder.prefix(2), timeout: 0.1) 398 | } 399 | } 400 | 401 | // FAIL: Caught error MyError 402 | func testPrefixError() throws { 403 | do { 404 | let publisher = PassthroughSubject() 405 | let recorder = publisher.record() 406 | publisher.send("foo") 407 | publisher.send(completion: .failure(MyError())) 408 | _ = try wait(for: recorder.prefix(2), timeout: 0.1) 409 | XCTFail("Expected error") 410 | } catch is MyError { } 411 | } 412 | 413 | // MARK: - Prefix.inverted 414 | 415 | // SUCCESS: no timeout, no error 416 | func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { 417 | let publisher = PassthroughSubject() 418 | let recorder = publisher.record() 419 | publisher.send("foo") 420 | publisher.send("bar") 421 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.1) 422 | XCTAssertEqual(elements, ["foo", "bar"]) 423 | } 424 | 425 | // FAIL: Fulfilled inverted expectation 426 | func testInvertedPrefixTooEarly() throws { 427 | try assertFailure("Fulfilled inverted expectation") { 428 | let publisher = PassthroughSubject() 429 | let recorder = publisher.record() 430 | publisher.send("foo") 431 | publisher.send("bar") 432 | publisher.send("baz") 433 | _ = try wait(for: recorder.prefix(3).inverted, timeout: 0.1) 434 | } 435 | } 436 | 437 | // FAIL: Fulfilled inverted expectation 438 | // FAIL: Caught error MyError 439 | func testInvertedPrefixError() throws { 440 | try assertFailure("Fulfilled inverted expectation") { 441 | do { 442 | let publisher = PassthroughSubject() 443 | let recorder = publisher.record() 444 | publisher.send("foo") 445 | publisher.send(completion: .failure(MyError())) 446 | _ = try wait(for: recorder.prefix(3).inverted, timeout: 0.1) 447 | XCTFail("Expected error") 448 | } catch is MyError { } 449 | } 450 | } 451 | 452 | // MARK: - Recording 453 | 454 | // SUCCESS: no error 455 | func testArrayPublisherSynchronousRecording() throws { 456 | let publisher = ["foo", "bar", "baz"].publisher 457 | let recorder = publisher.record() 458 | let recording = try recorder.recording.get() 459 | XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 460 | if case let .failure(error) = recording.completion { 461 | XCTFail("Unexpected error \(error)") 462 | } 463 | } 464 | 465 | // SUCCESS: no timeout, no error 466 | func testArrayPublisherRecording() throws { 467 | let publisher = ["foo", "bar", "baz"].publisher 468 | let recorder = publisher.record() 469 | let recording = try wait(for: recorder.recording, timeout: 0.1) 470 | XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) 471 | if case let .failure(error) = recording.completion { 472 | XCTFail("Unexpected error \(error)") 473 | } 474 | } 475 | 476 | // FAIL: Asynchronous wait failed 477 | // FAIL: Caught error RecordingError.notCompleted 478 | func testRecordingTimeout() throws { 479 | try assertFailure("Asynchronous wait failed") { 480 | do { 481 | let publisher = PassthroughSubject() 482 | let recorder = publisher.record() 483 | _ = try wait(for: recorder.recording, timeout: 0.1) 484 | XCTFail("Expected error") 485 | } catch RecordingError.notCompleted { } 486 | } 487 | } 488 | 489 | // MARK: - Single 490 | 491 | // SUCCESS: no error 492 | func testJustSynchronouslyPublishesExactlyOneElement() throws { 493 | let publisher = Just("foo") 494 | let recorder = publisher.record() 495 | let element = try recorder.single.get() 496 | XCTAssertEqual(element, "foo") 497 | } 498 | 499 | // SUCCESS: no timeout, no error 500 | func testJustPublishesExactlyOneElement() throws { 501 | let publisher = Just("foo") 502 | let recorder = publisher.record() 503 | let element = try wait(for: recorder.single, timeout: 0.1) 504 | XCTAssertEqual(element, "foo") 505 | } 506 | 507 | // FAIL: Asynchronous wait failed 508 | // FAIL: Caught error RecordingError.notCompleted 509 | func testSingleTimeout() throws { 510 | try assertFailure("Asynchronous wait failed") { 511 | do { 512 | let publisher = PassthroughSubject() 513 | let recorder = publisher.record() 514 | _ = try wait(for: recorder.single, timeout: 0.1) 515 | XCTFail("Expected error") 516 | } catch RecordingError.notCompleted { } 517 | } 518 | } 519 | 520 | // FAIL: Caught error MyError 521 | func testSingleError() throws { 522 | do { 523 | let publisher = PassthroughSubject() 524 | let recorder = publisher.record() 525 | publisher.send(completion: .failure(MyError())) 526 | _ = try wait(for: recorder.single, timeout: 0.1) 527 | XCTFail("Expected error") 528 | } catch is MyError { } 529 | } 530 | 531 | // FAIL: Caught error RecordingError.tooManyElements 532 | func testSingleTooManyElementsError() throws { 533 | do { 534 | let publisher = PassthroughSubject() 535 | let recorder = publisher.record() 536 | publisher.send("foo") 537 | publisher.send("bar") 538 | publisher.send(completion: .finished) 539 | _ = try wait(for: recorder.single, timeout: 0.1) 540 | XCTFail("Expected error") 541 | } catch RecordingError.tooManyElements { } 542 | } 543 | 544 | // FAIL: Caught error RecordingError.notEnoughElements 545 | func testSingleNotEnoughElementsError() throws { 546 | do { 547 | let publisher = PassthroughSubject() 548 | let recorder = publisher.record() 549 | publisher.send(completion: .finished) 550 | _ = try wait(for: recorder.single, timeout: 0.1) 551 | XCTFail("Expected error") 552 | } catch RecordingError.notEnoughElements { } 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /Tests/CombineExpectationsTests/FailureTestCase.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | /// A XCTestCase subclass that can test its own failures. 4 | class FailureTestCase: XCTestCase { 5 | private struct Failure: Hashable { 6 | #if compiler(>=5.3) 7 | let issue: XCTIssue 8 | #else 9 | var description: String 10 | var file: String 11 | var line: Int 12 | var expected: Bool 13 | #endif 14 | 15 | #if compiler(>=5.3) 16 | func issue(prefix: String = "") -> XCTIssue { 17 | if prefix.isEmpty { 18 | return issue 19 | } else { 20 | return XCTIssue( 21 | type: issue.type, 22 | compactDescription: "\(prefix): \(issue.compactDescription)", 23 | detailedDescription: issue.detailedDescription, 24 | sourceCodeContext: issue.sourceCodeContext, 25 | associatedError: issue.associatedError, 26 | attachments: issue.attachments) 27 | } 28 | } 29 | #else 30 | func failure(prefix: String = "") -> (description: String, file: String, line: Int, expected: Bool) { 31 | let prefix = prefix.isEmpty ? "" : "\(prefix): " 32 | return ( 33 | description: prefix + description, 34 | file: file, 35 | line: line, 36 | expected: expected) 37 | } 38 | #endif 39 | 40 | #if compiler(>=5.3) 41 | private var description: String { 42 | return issue.compactDescription 43 | } 44 | #endif 45 | 46 | func hash(into hasher: inout Hasher) { 47 | hasher.combine(0) 48 | } 49 | 50 | static func == (lhs: Failure, rhs: Failure) -> Bool { 51 | lhs.description.hasPrefix(rhs.description) || rhs.description.hasPrefix(lhs.description) 52 | } 53 | } 54 | 55 | private var recordedFailures: [Failure] = [] 56 | private var isRecordingFailures = false 57 | 58 | func assertFailure(_ prefixes: String..., file: StaticString = #file, line: UInt = #line, _ execute: () throws -> Void) rethrows { 59 | let recordedFailures = try recordingFailures(execute) 60 | if prefixes.isEmpty { 61 | if recordedFailures.isEmpty { 62 | #if compiler(>=5.3) 63 | record(XCTIssue( 64 | type: .assertionFailure, 65 | compactDescription: "No failure did happen", 66 | detailedDescription: nil, 67 | sourceCodeContext: XCTSourceCodeContext( 68 | location: XCTSourceCodeLocation( 69 | filePath: String(describing: file), 70 | lineNumber: Int(line))), 71 | associatedError: nil, 72 | attachments: [])) 73 | #else 74 | recordFailure( 75 | withDescription: "No failure did happen", 76 | inFile: file.description, 77 | atLine: Int(line), 78 | expected: true) 79 | #endif 80 | } 81 | } else { 82 | let expectedFailures = prefixes.map { prefix -> Failure in 83 | #if compiler(>=5.3) 84 | return Failure(issue: XCTIssue( 85 | type: .assertionFailure, 86 | compactDescription: prefix, 87 | detailedDescription: nil, 88 | sourceCodeContext: XCTSourceCodeContext( 89 | location: XCTSourceCodeLocation( 90 | filePath: String(describing: file), 91 | lineNumber: Int(line))), 92 | associatedError: nil, 93 | attachments: [])) 94 | #else 95 | return Failure( 96 | description: prefix, 97 | file: String(describing: file), 98 | line: Int(line), 99 | expected: true) 100 | #endif 101 | } 102 | assertMatch( 103 | recordedFailures: recordedFailures, 104 | expectedFailures: expectedFailures) 105 | } 106 | } 107 | 108 | override func setUp() { 109 | super.setUp() 110 | isRecordingFailures = false 111 | recordedFailures = [] 112 | } 113 | 114 | #if compiler(>=5.3) 115 | override func record(_ issue: XCTIssue) { 116 | if isRecordingFailures { 117 | recordedFailures.append(Failure(issue: issue)) 118 | } else { 119 | super.record(issue) 120 | } 121 | } 122 | #else 123 | override func recordFailure(withDescription description: String, inFile filePath: String, atLine lineNumber: Int, expected: Bool) { 124 | if isRecordingFailures { 125 | recordedFailures.append(Failure( 126 | description: description, 127 | file: filePath, 128 | line: lineNumber, 129 | expected: expected)) 130 | } else { 131 | super.recordFailure( 132 | withDescription: description, 133 | inFile: filePath, 134 | atLine: lineNumber, 135 | expected: expected) 136 | } 137 | } 138 | #endif 139 | 140 | private func recordingFailures(_ execute: () throws -> Void) rethrows -> [Failure] { 141 | let oldRecordingFailures = isRecordingFailures 142 | let oldRecordedFailures = recordedFailures 143 | defer { 144 | isRecordingFailures = oldRecordingFailures 145 | recordedFailures = oldRecordedFailures 146 | } 147 | isRecordingFailures = true 148 | recordedFailures = [] 149 | try execute() 150 | let result = recordedFailures 151 | return result 152 | } 153 | 154 | private func assertMatch(recordedFailures: [Failure], expectedFailures: [Failure]) { 155 | let diff = expectedFailures.difference(from: recordedFailures).inferringMoves() 156 | for change in diff { 157 | switch change { 158 | case let .insert(offset: _, element: failure, associatedWith: nil): 159 | #if compiler(>=5.3) 160 | record(failure.issue(prefix: "Failure did not happen")) 161 | #else 162 | let failure = failure.failure(prefix: "Failure did not happen") 163 | recordFailure( 164 | withDescription: failure.description, 165 | inFile: failure.file, 166 | atLine: failure.line, 167 | expected: failure.expected) 168 | #endif 169 | case let .remove(offset: _, element: failure, associatedWith: nil): 170 | #if compiler(>=5.3) 171 | record(failure.issue()) 172 | #else 173 | let failure = failure.failure() 174 | recordFailure( 175 | withDescription: failure.description, 176 | inFile: failure.file, 177 | atLine: failure.line, 178 | expected: failure.expected) 179 | #endif 180 | default: 181 | break 182 | } 183 | } 184 | } 185 | } 186 | 187 | // MARK: - Tests 188 | 189 | class FailureTestCaseTests: FailureTestCase { 190 | func testEmptyTest() { 191 | } 192 | 193 | func testExpectedAnyFailure() { 194 | assertFailure { 195 | XCTFail("foo") 196 | } 197 | assertFailure { 198 | XCTFail("foo") 199 | XCTFail("bar") 200 | } 201 | } 202 | 203 | func testMissingAnyFailure() { 204 | assertFailure("No failure did happen") { 205 | assertFailure { 206 | } 207 | } 208 | } 209 | 210 | func testExpectedFailure() { 211 | assertFailure("failed - foo") { 212 | XCTFail("foo") 213 | } 214 | } 215 | 216 | func testExpectedFailureMatchesOnPrefix() { 217 | assertFailure("failed - foo") { 218 | XCTFail("foobarbaz") 219 | } 220 | } 221 | 222 | func testOrderOfExpectedFailureIsIgnored() { 223 | assertFailure("failed - foo", "failed - bar") { 224 | XCTFail("foo") 225 | XCTFail("bar") 226 | } 227 | assertFailure("failed - bar", "failed - foo") { 228 | XCTFail("foo") 229 | XCTFail("bar") 230 | } 231 | } 232 | 233 | func testExpectedFailureCanBeRepeated() { 234 | assertFailure("failed - foo", "failed - foo", "failed - bar") { 235 | XCTFail("foo") 236 | XCTFail("bar") 237 | XCTFail("foo") 238 | } 239 | } 240 | 241 | func testExactNumberOfRepetitionIsRequired() { 242 | assertFailure("Failure did not happen: failed - foo") { 243 | assertFailure("failed - foo", "failed - foo") { 244 | XCTFail("foo") 245 | } 246 | } 247 | assertFailure("failed - foo") { 248 | assertFailure("failed - foo", "failed - foo") { 249 | XCTFail("foo") 250 | XCTFail("foo") 251 | XCTFail("foo") 252 | } 253 | } 254 | } 255 | 256 | func testUnexpectedFailure() { 257 | assertFailure("Failure did not happen: failed - foo") { 258 | assertFailure("failed - foo") { 259 | } 260 | } 261 | } 262 | 263 | func testMissedFailure() { 264 | assertFailure("failed - bar") { 265 | assertFailure("failed - foo") { 266 | XCTFail("foo") 267 | XCTFail("bar") 268 | } 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /Tests/CombineExpectationsTests/LateSubscriptionTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | import Foundation 4 | @testable import CombineExpectations 5 | 6 | /// Tests for subscribers that do not create subscriptions right when they 7 | /// receive subscribers. 8 | class LateSubscriptionTest: FailureTestCase { 9 | func testNoSubscriptionPublisher() throws { 10 | struct NoSubscriptionPublisher: Publisher { 11 | typealias Output = String 12 | typealias Failure = Never 13 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { } 14 | } 15 | 16 | do { 17 | let publisher = NoSubscriptionPublisher() 18 | let recorder = publisher.record() 19 | 20 | let (elements, completion) = recorder.elementsAndCompletion 21 | XCTAssertTrue(elements.isEmpty) 22 | XCTAssertNil(completion) 23 | } 24 | do { 25 | // a test with an expectation that is fulfilled on completion 26 | let publisher = NoSubscriptionPublisher() 27 | let recorder = publisher.record() 28 | 29 | try wait(for: recorder.finished.inverted, timeout: 0.1) 30 | } 31 | do { 32 | // a test with an expectation that is fulfilled on input 33 | let publisher = NoSubscriptionPublisher() 34 | let recorder = publisher.record() 35 | 36 | try wait(for: recorder.next().inverted, timeout: 0.1) 37 | } 38 | } 39 | 40 | func testAsynchronousSubscriptionPublisher() throws { 41 | struct AsynchronousSubscriptionPublisher: Publisher { 42 | typealias Output = Base.Output 43 | typealias Failure = Base.Failure 44 | let base: Base 45 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 46 | DispatchQueue.main.async { 47 | self.base.receive(subscriber: subscriber) 48 | } 49 | } 50 | } 51 | 52 | do { 53 | let publisher = AsynchronousSubscriptionPublisher(base: Just("foo")) 54 | let recorder = publisher.record() 55 | 56 | let (elements, completion) = recorder.elementsAndCompletion 57 | XCTAssertTrue(elements.isEmpty) 58 | XCTAssertNil(completion) 59 | } 60 | do { 61 | // a test with an expectation that is fulfilled on completion 62 | let publisher = AsynchronousSubscriptionPublisher(base: Just("foo")) 63 | let recorder = publisher.record() 64 | 65 | try wait(for: recorder.finished, timeout: 0.1) 66 | } 67 | do { 68 | // a test with an expectation that is fulfilled on input 69 | let publisher = AsynchronousSubscriptionPublisher(base: Just("foo")) 70 | let recorder = publisher.record() 71 | 72 | let element = try wait(for: recorder.next(), timeout: 0.1) 73 | XCTAssertEqual(element, "foo") 74 | 75 | let (elements, completion) = recorder.elementsAndCompletion 76 | XCTAssertEqual(elements, ["foo"]) 77 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/CombineExpectationsTests/RecorderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | import Foundation 4 | @testable import CombineExpectations 5 | 6 | /// General tests for publisher expectations 7 | class RecorderTests: XCTestCase { 8 | private struct TestError: Error { } 9 | 10 | // MARK: - Subscription 11 | 12 | func testRecorderSubscribes() throws { 13 | var subscribed = false 14 | let publisher = Empty().handleEvents(receiveSubscription: { _ in subscribed = true }) 15 | _ = publisher.record() 16 | XCTAssertTrue(subscribed) 17 | } 18 | 19 | // MARK: - availableElements 20 | 21 | func testAvailableElementsSync() throws { 22 | do { 23 | let publisher = [1, 2, 3].publisher 24 | let recorder = publisher.record() 25 | let availableElements = try recorder.availableElements.get() 26 | XCTAssertEqual(availableElements, [1, 2, 3]) 27 | } 28 | do { 29 | let publisher = PassthroughSubject() 30 | let recorder = publisher.record() 31 | publisher.send(1) 32 | publisher.send(2) 33 | publisher.send(3) 34 | let availableElements = try recorder.availableElements.get() 35 | XCTAssertEqual(availableElements, [1, 2, 3]) 36 | } 37 | do { 38 | let publisher = PassthroughSubject() 39 | let recorder = publisher.record() 40 | publisher.send(1) 41 | publisher.send(completion: .failure(TestError())) 42 | _ = try recorder.availableElements.get() 43 | XCTFail("Expected TestError") 44 | } catch is TestError { } 45 | } 46 | 47 | func testAvailableElementsAsync() throws { 48 | do { 49 | let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() 50 | let recorder = publisher.record() 51 | let dates = try wait(for: recorder.availableElements, timeout: 1) 52 | XCTAssertTrue(dates.count > 2) 53 | XCTAssertEqual(dates.sorted(), dates) 54 | } 55 | } 56 | 57 | func testAvailableElementsStopsOnPublisherCompletion() throws { 58 | do { 59 | let publisher = PassthroughSubject() 60 | let recorder = publisher.record() 61 | 62 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 63 | publisher.send(completion: .finished) 64 | } 65 | let start = Date() 66 | _ = try wait(for: recorder.availableElements, timeout: 2) 67 | let duration = Date().timeIntervalSince(start) 68 | XCTAssertLessThan(duration, 1) 69 | } 70 | } 71 | 72 | // MARK: - elementsAndCompletion 73 | 74 | func testElementsAndCompletionSync() throws { 75 | do { 76 | let publisher = Empty() 77 | let recorder = publisher.record() 78 | 79 | let (elements, completion) = recorder.elementsAndCompletion 80 | XCTAssertEqual(elements, []) 81 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 82 | } 83 | do { 84 | let publisher = (0..<1).publisher 85 | let recorder = publisher.record() 86 | 87 | let (elements, completion) = recorder.elementsAndCompletion 88 | XCTAssertEqual(elements, [0]) 89 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 90 | } 91 | do { 92 | let publisher = (0..<2).publisher 93 | let recorder = publisher.record() 94 | 95 | let (elements, completion) = recorder.elementsAndCompletion 96 | XCTAssertEqual(elements, [0, 1]) 97 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 98 | } 99 | } 100 | 101 | func testElementsAndCompletionAsync() throws { 102 | do { 103 | let publisher = Empty().receive(on: DispatchQueue.main) 104 | let recorder = publisher.record() 105 | 106 | var (elements, completion) = recorder.elementsAndCompletion 107 | XCTAssertEqual(elements, []) 108 | XCTAssertNil(completion) 109 | 110 | _ = try wait(for: recorder.completion, timeout: 1) 111 | (elements, completion) = recorder.elementsAndCompletion 112 | XCTAssertEqual(elements, []) 113 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 114 | } 115 | do { 116 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 117 | let recorder = publisher.record() 118 | 119 | var (elements, completion) = recorder.elementsAndCompletion 120 | XCTAssertEqual(elements, []) 121 | XCTAssertNil(completion) 122 | 123 | _ = try wait(for: recorder.completion, timeout: 1) 124 | (elements, completion) = recorder.elementsAndCompletion 125 | XCTAssertEqual(elements, [0]) 126 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 127 | } 128 | do { 129 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 130 | let recorder = publisher.record() 131 | 132 | var (elements, completion) = recorder.elementsAndCompletion 133 | XCTAssertEqual(elements, []) 134 | XCTAssertNil(completion) 135 | 136 | _ = try wait(for: recorder.completion, timeout: 1) 137 | (elements, completion) = recorder.elementsAndCompletion 138 | XCTAssertEqual(elements, [0, 1]) 139 | if case let .failure(error) = try XCTUnwrap(completion) { throw error } 140 | } 141 | } 142 | 143 | func testElementsAndCompletionFailure() throws { 144 | do { 145 | let publisher = Fail(error: TestError()) 146 | let recorder = publisher.record() 147 | 148 | let (elements, completion) = recorder.elementsAndCompletion 149 | XCTAssertEqual(elements, []) 150 | if case .finished = try XCTUnwrap(completion) { 151 | XCTFail("Expected TestError") 152 | } 153 | } 154 | do { 155 | let publisher = (0..<1).publisher.append(error: TestError()) 156 | let recorder = publisher.record() 157 | 158 | let (elements, completion) = recorder.elementsAndCompletion 159 | XCTAssertEqual(elements, [0]) 160 | if case .finished = try XCTUnwrap(completion) { 161 | XCTFail("Expected TestError") 162 | } 163 | } 164 | do { 165 | let publisher = (0..<2).publisher.append(error: TestError()) 166 | let recorder = publisher.record() 167 | 168 | let (elements, completion) = recorder.elementsAndCompletion 169 | XCTAssertEqual(elements, [0, 1]) 170 | if case .finished = try XCTUnwrap(completion) { 171 | XCTFail("Expected TestError") 172 | } 173 | } 174 | } 175 | 176 | func testElementsAndCompletionFailureAsync() throws { 177 | do { 178 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 179 | let recorder = publisher.record() 180 | 181 | var (elements, completion) = recorder.elementsAndCompletion 182 | XCTAssertEqual(elements, []) 183 | XCTAssertNil(completion) 184 | 185 | _ = try wait(for: recorder.completion, timeout: 1) 186 | (elements, completion) = recorder.elementsAndCompletion 187 | XCTAssertEqual(elements, []) 188 | if case .finished = try XCTUnwrap(completion) { 189 | XCTFail("Expected TestError") 190 | } 191 | } 192 | do { 193 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 194 | let recorder = publisher.record() 195 | 196 | var (elements, completion) = recorder.elementsAndCompletion 197 | XCTAssertEqual(elements, []) 198 | XCTAssertNil(completion) 199 | 200 | _ = try wait(for: recorder.completion, timeout: 1) 201 | (elements, completion) = recorder.elementsAndCompletion 202 | XCTAssertEqual(elements, [0]) 203 | if case .finished = try XCTUnwrap(completion) { 204 | XCTFail("Expected TestError") 205 | } 206 | } 207 | do { 208 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 209 | let recorder = publisher.record() 210 | 211 | var (elements, completion) = recorder.elementsAndCompletion 212 | XCTAssertEqual(elements, []) 213 | XCTAssertNil(completion) 214 | 215 | _ = try wait(for: recorder.completion, timeout: 1) 216 | (elements, completion) = recorder.elementsAndCompletion 217 | XCTAssertEqual(elements, [0, 1]) 218 | if case .finished = try XCTUnwrap(completion) { 219 | XCTFail("Expected TestError") 220 | } 221 | } 222 | } 223 | 224 | // MARK: - wait(for: recorder.elements) 225 | 226 | func testWaitForElementsSync() throws { 227 | do { 228 | let publisher = Empty() 229 | let recorder = publisher.record() 230 | let elements = try wait(for: recorder.elements, timeout: 1) 231 | XCTAssertEqual(elements, []) 232 | } 233 | do { 234 | let publisher = (0..<1).publisher 235 | let recorder = publisher.record() 236 | let elements = try wait(for: recorder.elements, timeout: 1) 237 | XCTAssertEqual(elements, [0]) 238 | } 239 | do { 240 | let publisher = (0..<2).publisher 241 | let recorder = publisher.record() 242 | let elements = try wait(for: recorder.elements, timeout: 1) 243 | XCTAssertEqual(elements, [0, 1]) 244 | } 245 | } 246 | 247 | func testWaitForElementsAsync() throws { 248 | do { 249 | let publisher = Empty().receive(on: DispatchQueue.main) 250 | let recorder = publisher.record() 251 | let elements = try wait(for: recorder.elements, timeout: 1) 252 | XCTAssertEqual(elements, []) 253 | } 254 | do { 255 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 256 | let recorder = publisher.record() 257 | let elements = try wait(for: recorder.elements, timeout: 1) 258 | XCTAssertEqual(elements, [0]) 259 | } 260 | do { 261 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 262 | let recorder = publisher.record() 263 | let elements = try wait(for: recorder.elements, timeout: 1) 264 | XCTAssertEqual(elements, [0, 1]) 265 | } 266 | } 267 | 268 | func testWaitForElementsFailure() throws { 269 | do { 270 | let publisher = Fail(error: TestError()) 271 | let recorder = publisher.record() 272 | _ = try wait(for: recorder.elements, timeout: 1) 273 | XCTFail("Expected TestError") 274 | } catch is TestError { } 275 | do { 276 | let publisher = (0..<1).publisher.append(error: TestError()) 277 | let recorder = publisher.record() 278 | _ = try wait(for: recorder.elements, timeout: 1) 279 | XCTFail("Expected TestError") 280 | } catch is TestError { } 281 | do { 282 | let publisher = (0..<2).publisher.append(error: TestError()) 283 | let recorder = publisher.record() 284 | _ = try wait(for: recorder.elements, timeout: 1) 285 | XCTFail("Expected TestError") 286 | } catch is TestError { } 287 | } 288 | 289 | func testWaitForElementsFailureAsync() throws { 290 | do { 291 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 292 | let recorder = publisher.record() 293 | _ = try wait(for: recorder.elements, timeout: 1) 294 | XCTFail("Expected TestError") 295 | } catch is TestError { } 296 | do { 297 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 298 | let recorder = publisher.record() 299 | _ = try wait(for: recorder.elements, timeout: 1) 300 | XCTFail("Expected TestError") 301 | } catch is TestError { } 302 | do { 303 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 304 | let recorder = publisher.record() 305 | _ = try wait(for: recorder.elements, timeout: 1) 306 | XCTFail("Expected TestError") 307 | } catch is TestError { } 308 | } 309 | 310 | func testWaitForElementsAndWaitAgain() throws { 311 | let publisher = (0..<2).publisher 312 | let recorder = publisher.record() 313 | let elements = try wait(for: recorder.elements, timeout: 1) 314 | XCTAssertEqual(elements, [0, 1]) 315 | 316 | do { 317 | let elements = try wait(for: recorder.elements, timeout: 1) 318 | XCTAssertEqual(elements, [0, 1]) 319 | } 320 | 321 | do { 322 | let elements = try wait(for: recorder.prefix(3), timeout: 1) 323 | XCTAssertEqual(elements, [0, 1]) 324 | } 325 | 326 | do { 327 | let element = try wait(for: recorder.last, timeout: 1) 328 | XCTAssertEqual(element, 1) 329 | } 330 | 331 | do { 332 | _ = try wait(for: recorder.single, timeout: 1) 333 | XCTFail("Expected RecordingError") 334 | } catch RecordingError.tooManyElements { } 335 | 336 | do { 337 | try wait(for: recorder.finished, timeout: 1) 338 | } 339 | 340 | do { 341 | let completion = try wait(for: recorder.completion, timeout: 1) 342 | if case let .failure(error) = completion { throw error } 343 | } 344 | } 345 | 346 | // MARK: - wait(for: recorder.next()) 347 | 348 | func testWaitForNextSync() throws { 349 | do { 350 | let publisher = Empty() 351 | let recorder = publisher.record() 352 | _ = try wait(for: recorder.next(), timeout: 1) 353 | XCTFail("Expected RecordingError") 354 | } catch RecordingError.notEnoughElements { } 355 | do { 356 | let publisher = (0..<1).publisher 357 | let recorder = publisher.record() 358 | let element = try wait(for: recorder.next(), timeout: 1) 359 | XCTAssertEqual(element, 0) 360 | } 361 | do { 362 | let publisher = (0..<2).publisher 363 | let recorder = publisher.record() 364 | let element = try wait(for: recorder.next(), timeout: 1) 365 | XCTAssertEqual(element, 0) 366 | } 367 | } 368 | 369 | func testWaitForNextAsync() throws { 370 | do { 371 | let publisher = Empty().receive(on: DispatchQueue.main) 372 | let recorder = publisher.record() 373 | _ = try wait(for: recorder.next(), timeout: 1) 374 | XCTFail("Expected RecordingError") 375 | } catch RecordingError.notEnoughElements { } 376 | do { 377 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 378 | let recorder = publisher.record() 379 | let element = try wait(for: recorder.next(), timeout: 1) 380 | XCTAssertEqual(element, 0) 381 | } 382 | do { 383 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 384 | let recorder = publisher.record() 385 | let element = try wait(for: recorder.next(), timeout: 1) 386 | XCTAssertEqual(element, 0) 387 | } 388 | } 389 | 390 | func testWaitForNextFailure() throws { 391 | do { 392 | let publisher = Fail(error: TestError()) 393 | let recorder = publisher.record() 394 | _ = try wait(for: recorder.next(), timeout: 1) 395 | XCTFail("Expected TestError") 396 | } catch is TestError { } 397 | do { 398 | let publisher = (0..<1).publisher.append(error: TestError()) 399 | let recorder = publisher.record() 400 | let element = try wait(for: recorder.next(), timeout: 1) 401 | XCTAssertEqual(element, 0) 402 | } 403 | do { 404 | let publisher = (0..<2).publisher.append(error: TestError()) 405 | let recorder = publisher.record() 406 | let element = try wait(for: recorder.next(), timeout: 1) 407 | XCTAssertEqual(element, 0) 408 | } 409 | } 410 | 411 | func testWaitForNextFailureAsync() throws { 412 | do { 413 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 414 | let recorder = publisher.record() 415 | _ = try wait(for: recorder.next(), timeout: 1) 416 | XCTFail("Expected TestError") 417 | } catch is TestError { } 418 | do { 419 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 420 | let recorder = publisher.record() 421 | let element = try wait(for: recorder.next(), timeout: 1) 422 | XCTAssertEqual(element, 0) 423 | } 424 | do { 425 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 426 | let recorder = publisher.record() 427 | let element = try wait(for: recorder.next(), timeout: 1) 428 | XCTAssertEqual(element, 0) 429 | } 430 | } 431 | 432 | func testWaitForNextInverted() throws { 433 | do { 434 | let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) 435 | let recorder = publisher.record() 436 | try wait(for: recorder.next().inverted, timeout: 0.01) 437 | } 438 | do { 439 | let publisher = Timer.publish(every: 0.2, on: .main, in: .default).autoconnect() 440 | let recorder = publisher.record() 441 | try wait(for: recorder.next().inverted, timeout: 0.1) 442 | _ = try wait(for: recorder.next(), timeout: 1) 443 | } 444 | } 445 | 446 | func testNextNext() throws { 447 | do { 448 | let publisher = PassthroughSubject() 449 | let recorder = publisher.record() 450 | publisher.send(0) 451 | publisher.send(1) 452 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 0) 453 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) 454 | } 455 | do { 456 | let publisher = PassthroughSubject() 457 | let recorder = publisher.record() 458 | publisher.send(0) 459 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 0) 460 | publisher.send(1) 461 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) 462 | } 463 | do { 464 | let publisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect() 465 | let recorder = publisher.record() 466 | _ = try wait(for: recorder.next(), timeout: 1) 467 | _ = try wait(for: recorder.next(), timeout: 1) 468 | } 469 | } 470 | 471 | func testPrefixNext() throws { 472 | let publisher = PassthroughSubject() 473 | let recorder = publisher.record() 474 | publisher.send(0) 475 | publisher.send(1) 476 | publisher.send(2) 477 | try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) 478 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 2) 479 | try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) 480 | publisher.send(3) 481 | publisher.send(4) 482 | publisher.send(5) 483 | try XCTAssertEqual(wait(for: recorder.prefix(4), timeout: 1), [0, 1, 2, 3]) 484 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 4) 485 | try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 5) 486 | } 487 | 488 | // MARK: - wait(for: recorder.next(0)) 489 | 490 | func testWaitForNext0Sync() throws { 491 | do { 492 | let publisher = Empty() 493 | let recorder = publisher.record() 494 | let elements = try wait(for: recorder.next(0), timeout: 1) 495 | XCTAssertEqual(elements, []) 496 | } 497 | do { 498 | let publisher = (0..<1).publisher 499 | let recorder = publisher.record() 500 | let elements = try wait(for: recorder.next(0), timeout: 1) 501 | XCTAssertEqual(elements, []) 502 | } 503 | do { 504 | let publisher = (0..<2).publisher 505 | let recorder = publisher.record() 506 | let elements = try wait(for: recorder.next(0), timeout: 1) 507 | XCTAssertEqual(elements, []) 508 | } 509 | } 510 | 511 | func testWaitForNext0Async() throws { 512 | do { 513 | let publisher = Empty().receive(on: DispatchQueue.main) 514 | let recorder = publisher.record() 515 | let elements = try wait(for: recorder.next(0), timeout: 1) 516 | XCTAssertEqual(elements, []) 517 | } 518 | do { 519 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 520 | let recorder = publisher.record() 521 | let elements = try wait(for: recorder.next(0), timeout: 1) 522 | XCTAssertEqual(elements, []) 523 | } 524 | do { 525 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 526 | let recorder = publisher.record() 527 | let elements = try wait(for: recorder.next(0), timeout: 1) 528 | XCTAssertEqual(elements, []) 529 | } 530 | } 531 | 532 | func testWaitForNext0Failure() throws { 533 | do { 534 | let publisher = Fail(error: TestError()) 535 | let recorder = publisher.record() 536 | let elements = try wait(for: recorder.next(0), timeout: 1) 537 | XCTAssertEqual(elements, []) 538 | } catch is TestError { } 539 | do { 540 | let publisher = (0..<1).publisher.append(error: TestError()) 541 | let recorder = publisher.record() 542 | let elements = try wait(for: recorder.next(0), timeout: 1) 543 | XCTAssertEqual(elements, []) 544 | } catch is TestError { } 545 | do { 546 | let publisher = (0..<2).publisher.append(error: TestError()) 547 | let recorder = publisher.record() 548 | let elements = try wait(for: recorder.next(0), timeout: 1) 549 | XCTAssertEqual(elements, []) 550 | } catch is TestError { } 551 | } 552 | 553 | func testWaitForNext0FailureAsync() throws { 554 | do { 555 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 556 | let recorder = publisher.record() 557 | let elements = try wait(for: recorder.next(0), timeout: 1) 558 | XCTAssertEqual(elements, []) 559 | } catch is TestError { } 560 | do { 561 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 562 | let recorder = publisher.record() 563 | let elements = try wait(for: recorder.next(0), timeout: 1) 564 | XCTAssertEqual(elements, []) 565 | } catch is TestError { } 566 | do { 567 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 568 | let recorder = publisher.record() 569 | let elements = try wait(for: recorder.next(0), timeout: 1) 570 | XCTAssertEqual(elements, []) 571 | } catch is TestError { } 572 | } 573 | 574 | // MARK: - wait(for: recorder.next(2)) 575 | 576 | func testWaitForNext2Sync() throws { 577 | do { 578 | let publisher = Empty() 579 | let recorder = publisher.record() 580 | _ = try wait(for: recorder.next(2), timeout: 1) 581 | XCTFail("Expected RecordingError") 582 | } catch RecordingError.notEnoughElements { } 583 | do { 584 | let publisher = (0..<1).publisher 585 | let recorder = publisher.record() 586 | _ = try wait(for: recorder.next(2), timeout: 1) 587 | XCTFail("Expected RecordingError") 588 | } catch RecordingError.notEnoughElements { } 589 | do { 590 | let publisher = (0..<2).publisher 591 | let recorder = publisher.record() 592 | let elements = try wait(for: recorder.next(2), timeout: 1) 593 | XCTAssertEqual(elements, [0, 1]) 594 | } 595 | do { 596 | let publisher = (0..<3).publisher 597 | let recorder = publisher.record() 598 | let elements = try wait(for: recorder.next(2), timeout: 1) 599 | XCTAssertEqual(elements, [0, 1]) 600 | } 601 | } 602 | 603 | func testWaitForNext2Async() throws { 604 | do { 605 | let publisher = Empty().receive(on: DispatchQueue.main) 606 | let recorder = publisher.record() 607 | _ = try wait(for: recorder.next(2), timeout: 1) 608 | XCTFail("Expected RecordingError") 609 | } catch RecordingError.notEnoughElements { } 610 | do { 611 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 612 | let recorder = publisher.record() 613 | _ = try wait(for: recorder.next(2), timeout: 1) 614 | XCTFail("Expected RecordingError") 615 | } catch RecordingError.notEnoughElements { } 616 | do { 617 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 618 | let recorder = publisher.record() 619 | let elements = try wait(for: recorder.next(2), timeout: 1) 620 | XCTAssertEqual(elements, [0, 1]) 621 | } 622 | do { 623 | let publisher = (0..<3).publisher.receive(on: DispatchQueue.main) 624 | let recorder = publisher.record() 625 | let elements = try wait(for: recorder.next(2), timeout: 1) 626 | XCTAssertEqual(elements, [0, 1]) 627 | } 628 | } 629 | 630 | func testWaitForNext2Failure() throws { 631 | do { 632 | let publisher = Fail(error: TestError()) 633 | let recorder = publisher.record() 634 | _ = try wait(for: recorder.next(2), timeout: 1) 635 | XCTFail("Expected TestError") 636 | } catch is TestError { } 637 | do { 638 | let publisher = (0..<1).publisher.append(error: TestError()) 639 | let recorder = publisher.record() 640 | _ = try wait(for: recorder.next(2), timeout: 1) 641 | XCTFail("Expected TestError") 642 | } catch is TestError { } 643 | do { 644 | let publisher = (0..<2).publisher.append(error: TestError()) 645 | let recorder = publisher.record() 646 | let elements = try wait(for: recorder.next(2), timeout: 1) 647 | XCTAssertEqual(elements, [0, 1]) 648 | } 649 | do { 650 | let publisher = (0..<3).publisher.append(error: TestError()) 651 | let recorder = publisher.record() 652 | let elements = try wait(for: recorder.next(2), timeout: 1) 653 | XCTAssertEqual(elements, [0, 1]) 654 | } 655 | } 656 | 657 | func testWaitForNext2FailureAsync() throws { 658 | do { 659 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 660 | let recorder = publisher.record() 661 | _ = try wait(for: recorder.next(2), timeout: 1) 662 | XCTFail("Expected TestError") 663 | } catch is TestError { } 664 | do { 665 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 666 | let recorder = publisher.record() 667 | _ = try wait(for: recorder.next(2), timeout: 1) 668 | XCTFail("Expected TestError") 669 | } catch is TestError { } 670 | do { 671 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 672 | let recorder = publisher.record() 673 | let elements = try wait(for: recorder.next(2), timeout: 1) 674 | XCTAssertEqual(elements, [0, 1]) 675 | } 676 | do { 677 | let publisher = (0..<3).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 678 | let recorder = publisher.record() 679 | let elements = try wait(for: recorder.next(2), timeout: 1) 680 | XCTAssertEqual(elements, [0, 1]) 681 | } 682 | } 683 | 684 | func testNext2Next2() throws { 685 | do { 686 | let publisher = PassthroughSubject() 687 | let recorder = publisher.record() 688 | publisher.send(0) 689 | publisher.send(1) 690 | publisher.send(2) 691 | publisher.send(3) 692 | try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [0, 1]) 693 | try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [2, 3]) 694 | } 695 | do { 696 | let publisher = PassthroughSubject() 697 | let recorder = publisher.record() 698 | publisher.send(0) 699 | publisher.send(1) 700 | try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [0, 1]) 701 | publisher.send(2) 702 | publisher.send(3) 703 | try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [2, 3]) 704 | } 705 | do { 706 | let publisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect() 707 | let recorder = publisher.record() 708 | _ = try wait(for: recorder.next(2), timeout: 1) 709 | _ = try wait(for: recorder.next(2), timeout: 1) 710 | } 711 | } 712 | 713 | func testPrefixNext2() throws { 714 | let publisher = PassthroughSubject() 715 | let recorder = publisher.record() 716 | publisher.send(0) 717 | publisher.send(1) 718 | publisher.send(2) 719 | publisher.send(3) 720 | try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) 721 | try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [2, 3]) 722 | try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) 723 | publisher.send(4) 724 | publisher.send(5) 725 | try XCTAssertEqual(wait(for: recorder.prefix(4), timeout: 1), [0, 1, 2, 3]) 726 | try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [4, 5]) 727 | } 728 | 729 | // MARK: - wait(for: recorder.prefix(0)) 730 | 731 | func testWaitForPrefix0Sync() throws { 732 | do { 733 | let publisher = Empty() 734 | let recorder = publisher.record() 735 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 736 | XCTAssertEqual(elements, []) 737 | } 738 | do { 739 | let publisher = (0..<1).publisher 740 | let recorder = publisher.record() 741 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 742 | XCTAssertEqual(elements, []) 743 | } 744 | do { 745 | let publisher = (0..<2).publisher 746 | let recorder = publisher.record() 747 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 748 | XCTAssertEqual(elements, []) 749 | } 750 | } 751 | 752 | func testWaitForPrefix0Async() throws { 753 | do { 754 | let publisher = Empty().receive(on: DispatchQueue.main) 755 | let recorder = publisher.record() 756 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 757 | XCTAssertEqual(elements, []) 758 | } 759 | do { 760 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 761 | let recorder = publisher.record() 762 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 763 | XCTAssertEqual(elements, []) 764 | } 765 | do { 766 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 767 | let recorder = publisher.record() 768 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 769 | XCTAssertEqual(elements, []) 770 | } 771 | } 772 | 773 | func testWaitForPrefix0Failure() throws { 774 | do { 775 | let publisher = Fail(error: TestError()) 776 | let recorder = publisher.record() 777 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 778 | XCTAssertEqual(elements, []) 779 | } catch is TestError { } 780 | do { 781 | let publisher = (0..<1).publisher.append(error: TestError()) 782 | let recorder = publisher.record() 783 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 784 | XCTAssertEqual(elements, []) 785 | } catch is TestError { } 786 | do { 787 | let publisher = (0..<2).publisher.append(error: TestError()) 788 | let recorder = publisher.record() 789 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 790 | XCTAssertEqual(elements, []) 791 | } catch is TestError { } 792 | } 793 | 794 | func testWaitForPrefix0FailureAsync() throws { 795 | do { 796 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 797 | let recorder = publisher.record() 798 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 799 | XCTAssertEqual(elements, []) 800 | } catch is TestError { } 801 | do { 802 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 803 | let recorder = publisher.record() 804 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 805 | XCTAssertEqual(elements, []) 806 | } catch is TestError { } 807 | do { 808 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 809 | let recorder = publisher.record() 810 | let elements = try wait(for: recorder.prefix(0), timeout: 1) 811 | XCTAssertEqual(elements, []) 812 | } catch is TestError { } 813 | } 814 | 815 | // MARK: - wait(for: recorder.prefix(1)) 816 | 817 | func testWaitForPrefix1Sync() throws { 818 | do { 819 | let publisher = Empty() 820 | let recorder = publisher.record() 821 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 822 | XCTAssertEqual(elements, []) 823 | } 824 | do { 825 | let publisher = (0..<1).publisher 826 | let recorder = publisher.record() 827 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 828 | XCTAssertEqual(elements, [0]) 829 | } 830 | do { 831 | let publisher = (0..<2).publisher 832 | let recorder = publisher.record() 833 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 834 | XCTAssertEqual(elements, [0]) 835 | } 836 | } 837 | 838 | func testWaitForPrefix1Async() throws { 839 | do { 840 | let publisher = Empty().receive(on: DispatchQueue.main) 841 | let recorder = publisher.record() 842 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 843 | XCTAssertEqual(elements, []) 844 | } 845 | do { 846 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 847 | let recorder = publisher.record() 848 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 849 | XCTAssertEqual(elements, [0]) 850 | } 851 | do { 852 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 853 | let recorder = publisher.record() 854 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 855 | XCTAssertEqual(elements, [0]) 856 | } 857 | } 858 | 859 | func testWaitForPrefix1Failure() throws { 860 | do { 861 | let publisher = Fail(error: TestError()) 862 | let recorder = publisher.record() 863 | _ = try wait(for: recorder.prefix(1), timeout: 1) 864 | XCTFail("Expected TestError") 865 | } catch is TestError { } 866 | do { 867 | let publisher = (0..<1).publisher.append(error: TestError()) 868 | let recorder = publisher.record() 869 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 870 | XCTAssertEqual(elements, [0]) 871 | } catch is TestError { } 872 | do { 873 | let publisher = (0..<2).publisher.append(error: TestError()) 874 | let recorder = publisher.record() 875 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 876 | XCTAssertEqual(elements, [0]) 877 | } catch is TestError { } 878 | } 879 | 880 | func testWaitForPrefix1FailureAsync() throws { 881 | do { 882 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 883 | let recorder = publisher.record() 884 | _ = try wait(for: recorder.prefix(1), timeout: 1) 885 | XCTFail("Expected TestError") 886 | } catch is TestError { } 887 | do { 888 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 889 | let recorder = publisher.record() 890 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 891 | XCTAssertEqual(elements, [0]) 892 | } catch is TestError { } 893 | do { 894 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 895 | let recorder = publisher.record() 896 | let elements = try wait(for: recorder.prefix(1), timeout: 1) 897 | XCTAssertEqual(elements, [0]) 898 | } catch is TestError { } 899 | } 900 | 901 | func testWaitForPrefix1Inverted() throws { 902 | do { 903 | let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) 904 | let recorder = publisher.record() 905 | let elements = try wait(for: recorder.prefix(1).inverted, timeout: 0.01) 906 | XCTAssertEqual(elements, []) 907 | } 908 | } 909 | 910 | // MARK: - wait(for: recorder.prefix(2)) 911 | 912 | func testWaitForPrefix2Sync() throws { 913 | do { 914 | let publisher = Empty() 915 | let recorder = publisher.record() 916 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 917 | XCTAssertEqual(elements, []) 918 | } 919 | do { 920 | let publisher = (0..<1).publisher 921 | let recorder = publisher.record() 922 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 923 | XCTAssertEqual(elements, [0]) 924 | } 925 | do { 926 | let publisher = (0..<2).publisher 927 | let recorder = publisher.record() 928 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 929 | XCTAssertEqual(elements, [0, 1]) 930 | } 931 | do { 932 | let publisher = (0..<3).publisher 933 | let recorder = publisher.record() 934 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 935 | XCTAssertEqual(elements, [0, 1]) 936 | } 937 | } 938 | 939 | func testWaitForPrefix2Async() throws { 940 | do { 941 | let publisher = Empty().receive(on: DispatchQueue.main) 942 | let recorder = publisher.record() 943 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 944 | XCTAssertEqual(elements, []) 945 | } 946 | do { 947 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 948 | let recorder = publisher.record() 949 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 950 | XCTAssertEqual(elements, [0]) 951 | } 952 | do { 953 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 954 | let recorder = publisher.record() 955 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 956 | XCTAssertEqual(elements, [0, 1]) 957 | } 958 | do { 959 | let publisher = (0..<3).publisher.receive(on: DispatchQueue.main) 960 | let recorder = publisher.record() 961 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 962 | XCTAssertEqual(elements, [0, 1]) 963 | } 964 | } 965 | 966 | func testWaitForPrefix2Failure() throws { 967 | do { 968 | let publisher = Fail(error: TestError()) 969 | let recorder = publisher.record() 970 | _ = try wait(for: recorder.prefix(2), timeout: 1) 971 | XCTFail("Expected TestError") 972 | } catch is TestError { } 973 | do { 974 | let publisher = (0..<1).publisher.append(error: TestError()) 975 | let recorder = publisher.record() 976 | _ = try wait(for: recorder.prefix(2), timeout: 1) 977 | XCTFail("Expected TestError") 978 | } catch is TestError { } 979 | do { 980 | let publisher = (0..<2).publisher.append(error: TestError()) 981 | let recorder = publisher.record() 982 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 983 | XCTAssertEqual(elements, [0, 1]) 984 | } catch is TestError { } 985 | do { 986 | let publisher = (0..<3).publisher.append(error: TestError()) 987 | let recorder = publisher.record() 988 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 989 | XCTAssertEqual(elements, [0, 1]) 990 | } catch is TestError { } 991 | } 992 | 993 | func testWaitForPrefix2FailureAsync() throws { 994 | do { 995 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 996 | let recorder = publisher.record() 997 | _ = try wait(for: recorder.prefix(2), timeout: 1) 998 | XCTFail("Expected TestError") 999 | } catch is TestError { } 1000 | do { 1001 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1002 | let recorder = publisher.record() 1003 | _ = try wait(for: recorder.prefix(2), timeout: 1) 1004 | XCTFail("Expected TestError") 1005 | } catch is TestError { } 1006 | do { 1007 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1008 | let recorder = publisher.record() 1009 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 1010 | XCTAssertEqual(elements, [0, 1]) 1011 | } catch is TestError { } 1012 | do { 1013 | let publisher = (0..<3).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1014 | let recorder = publisher.record() 1015 | let elements = try wait(for: recorder.prefix(2), timeout: 1) 1016 | XCTAssertEqual(elements, [0, 1]) 1017 | } catch is TestError { } 1018 | } 1019 | 1020 | func testWaitForPrefix2Inverted() throws { 1021 | do { 1022 | let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) 1023 | let recorder = publisher.record() 1024 | let elements = try wait(for: recorder.prefix(2).inverted, timeout: 0.01) 1025 | XCTAssertEqual(elements, []) 1026 | } 1027 | do { 1028 | let publisher = (0..<1).publisher.append(Empty().delay(for: 0.1, scheduler: DispatchQueue.main)) 1029 | let recorder = publisher.record() 1030 | let elements = try wait(for: recorder.prefix(2).inverted, timeout: 0.01) 1031 | XCTAssertEqual(elements, [0]) 1032 | } 1033 | } 1034 | 1035 | // MARK: - wait(for: recorder.prefix(3)) 1036 | 1037 | func testWaitForPrefix3Inverted() throws { 1038 | do { 1039 | let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) 1040 | let recorder = publisher.record() 1041 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.01) 1042 | XCTAssertEqual(elements, []) 1043 | } 1044 | do { 1045 | let publisher = (0..<1).publisher.append(Empty().delay(for: 0.1, scheduler: DispatchQueue.main)) 1046 | let recorder = publisher.record() 1047 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.01) 1048 | XCTAssertEqual(elements, [0]) 1049 | } 1050 | do { 1051 | let publisher = (0..<2).publisher.append(Empty().delay(for: 0.1, scheduler: DispatchQueue.main)) 1052 | let recorder = publisher.record() 1053 | let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.0) 1054 | XCTAssertEqual(elements, [0, 1]) 1055 | } 1056 | } 1057 | 1058 | // MARK: - wait(for: recorder.prefix(N)) 1059 | 1060 | func testWaitForPrefixAndWaitForPrefixAgain() throws { 1061 | let publisher = PassthroughSubject() 1062 | let recorder = publisher.record() 1063 | publisher.send(0) 1064 | try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) 1065 | try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) 1066 | publisher.send(1) 1067 | try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) 1068 | publisher.send(2) 1069 | try XCTAssertEqual(wait(for: recorder.prefix(3), timeout: 1), [0, 1, 2]) 1070 | } 1071 | 1072 | func testWaitForPrefixAndWaitForPrefixAgainInverted() throws { 1073 | let publisher = PassthroughSubject() 1074 | let recorder = publisher.record() 1075 | publisher.send(0) 1076 | try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) 1077 | try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) 1078 | try XCTAssertEqual(wait(for: recorder.prefix(2).inverted, timeout: 0.01), [0]) 1079 | } 1080 | 1081 | // MARK: - wait(for: recorder.last) 1082 | 1083 | func testWaitForLastSync() throws { 1084 | do { 1085 | let publisher = Empty() 1086 | let recorder = publisher.record() 1087 | let element = try wait(for: recorder.last, timeout: 1) 1088 | XCTAssertNil(element) 1089 | } 1090 | do { 1091 | let publisher = (0..<1).publisher 1092 | let recorder = publisher.record() 1093 | let element = try wait(for: recorder.last, timeout: 1) 1094 | XCTAssertEqual(element, 0) 1095 | } 1096 | do { 1097 | let publisher = (0..<2).publisher 1098 | let recorder = publisher.record() 1099 | let element = try wait(for: recorder.last, timeout: 1) 1100 | XCTAssertEqual(element, 1) 1101 | } 1102 | } 1103 | 1104 | func testWaitForLastAsync() throws { 1105 | do { 1106 | let publisher = Empty().receive(on: DispatchQueue.main) 1107 | let recorder = publisher.record() 1108 | let element = try wait(for: recorder.last, timeout: 1) 1109 | XCTAssertNil(element) 1110 | } 1111 | do { 1112 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 1113 | let recorder = publisher.record() 1114 | let element = try wait(for: recorder.last, timeout: 1) 1115 | XCTAssertEqual(element, 0) 1116 | } 1117 | do { 1118 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 1119 | let recorder = publisher.record() 1120 | let element = try wait(for: recorder.last, timeout: 1) 1121 | XCTAssertEqual(element, 1) 1122 | } 1123 | } 1124 | 1125 | func testWaitForLastFailure() throws { 1126 | do { 1127 | let publisher = Fail(error: TestError()) 1128 | let recorder = publisher.record() 1129 | _ = try wait(for: recorder.last, timeout: 1) 1130 | XCTFail("Expected TestError") 1131 | } catch is TestError { } 1132 | do { 1133 | let publisher = (0..<1).publisher.append(error: TestError()) 1134 | let recorder = publisher.record() 1135 | _ = try wait(for: recorder.last, timeout: 1) 1136 | XCTFail("Expected TestError") 1137 | } catch is TestError { } 1138 | do { 1139 | let publisher = (0..<2).publisher.append(error: TestError()) 1140 | let recorder = publisher.record() 1141 | _ = try wait(for: recorder.last, timeout: 1) 1142 | XCTFail("Expected TestError") 1143 | } catch is TestError { } 1144 | } 1145 | 1146 | func testWaitForLastFailureAsync() throws { 1147 | do { 1148 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 1149 | let recorder = publisher.record() 1150 | _ = try wait(for: recorder.last, timeout: 1) 1151 | XCTFail("Expected TestError") 1152 | } catch is TestError { } 1153 | do { 1154 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1155 | let recorder = publisher.record() 1156 | _ = try wait(for: recorder.last, timeout: 1) 1157 | XCTFail("Expected TestError") 1158 | } catch is TestError { } 1159 | do { 1160 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1161 | let recorder = publisher.record() 1162 | _ = try wait(for: recorder.last, timeout: 1) 1163 | XCTFail("Expected TestError") 1164 | } catch is TestError { } 1165 | } 1166 | 1167 | // MARK: - wait(for: recorder.single) 1168 | 1169 | func testWaitForSingleSync() throws { 1170 | do { 1171 | let publisher = Empty() 1172 | let recorder = publisher.record() 1173 | _ = try wait(for: recorder.single, timeout: 1) 1174 | XCTFail("Expected RecordingError") 1175 | } catch RecordingError.notEnoughElements { } 1176 | do { 1177 | let publisher = (0..<1).publisher 1178 | let recorder = publisher.record() 1179 | let element = try wait(for: recorder.single, timeout: 1) 1180 | XCTAssertEqual(element, 0) 1181 | } 1182 | do { 1183 | let publisher = (0..<2).publisher 1184 | let recorder = publisher.record() 1185 | _ = try wait(for: recorder.single, timeout: 1) 1186 | XCTFail("Expected RecordingError") 1187 | } catch RecordingError.tooManyElements { } 1188 | } 1189 | 1190 | func testWaitForSingleAsync() throws { 1191 | do { 1192 | let publisher = Empty().receive(on: DispatchQueue.main) 1193 | let recorder = publisher.record() 1194 | _ = try wait(for: recorder.single, timeout: 1) 1195 | XCTFail("Expected RecordingError") 1196 | } catch RecordingError.notEnoughElements { } 1197 | do { 1198 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 1199 | let recorder = publisher.record() 1200 | let element = try wait(for: recorder.single, timeout: 1) 1201 | XCTAssertEqual(element, 0) 1202 | } 1203 | do { 1204 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 1205 | let recorder = publisher.record() 1206 | _ = try wait(for: recorder.single, timeout: 1) 1207 | XCTFail("Expected RecordingError") 1208 | } catch RecordingError.tooManyElements { } 1209 | } 1210 | 1211 | func testWaitForSingleFailure() throws { 1212 | do { 1213 | let publisher = Fail(error: TestError()) 1214 | let recorder = publisher.record() 1215 | _ = try wait(for: recorder.single, timeout: 1) 1216 | XCTFail("Expected TestError") 1217 | } catch is TestError { } 1218 | do { 1219 | let publisher = (0..<1).publisher.append(error: TestError()) 1220 | let recorder = publisher.record() 1221 | _ = try wait(for: recorder.single, timeout: 1) 1222 | XCTFail("Expected TestError") 1223 | } catch is TestError { } 1224 | do { 1225 | let publisher = (0..<2).publisher.append(error: TestError()) 1226 | let recorder = publisher.record() 1227 | _ = try wait(for: recorder.single, timeout: 1) 1228 | XCTFail("Expected TestError") 1229 | } catch is TestError { } 1230 | } 1231 | 1232 | func testWaitForSingleFailureAsync() throws { 1233 | do { 1234 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 1235 | let recorder = publisher.record() 1236 | _ = try wait(for: recorder.single, timeout: 1) 1237 | XCTFail("Expected TestError") 1238 | } catch is TestError { } 1239 | do { 1240 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1241 | let recorder = publisher.record() 1242 | _ = try wait(for: recorder.single, timeout: 1) 1243 | XCTFail("Expected TestError") 1244 | } catch is TestError { } 1245 | do { 1246 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1247 | let recorder = publisher.record() 1248 | _ = try wait(for: recorder.single, timeout: 1) 1249 | XCTFail("Expected TestError") 1250 | } catch is TestError { } 1251 | } 1252 | 1253 | // MARK: - wait(for: recorder.finished) 1254 | 1255 | func testWaitForFinishedSync() throws { 1256 | do { 1257 | let publisher = Empty() 1258 | let recorder = publisher.record() 1259 | try wait(for: recorder.finished, timeout: 1) 1260 | } 1261 | do { 1262 | let publisher = (0..<1).publisher 1263 | let recorder = publisher.record() 1264 | try wait(for: recorder.finished, timeout: 1) 1265 | } 1266 | do { 1267 | let publisher = (0..<2).publisher 1268 | let recorder = publisher.record() 1269 | try wait(for: recorder.finished, timeout: 1) 1270 | } 1271 | } 1272 | 1273 | func testWaitForFinishedAsync() throws { 1274 | do { 1275 | let publisher = Empty().receive(on: DispatchQueue.main) 1276 | let recorder = publisher.record() 1277 | try wait(for: recorder.finished, timeout: 1) 1278 | } 1279 | do { 1280 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 1281 | let recorder = publisher.record() 1282 | try wait(for: recorder.finished, timeout: 1) 1283 | } 1284 | do { 1285 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 1286 | let recorder = publisher.record() 1287 | try wait(for: recorder.finished, timeout: 1) 1288 | } 1289 | } 1290 | 1291 | func testWaitForFinishedFailure() throws { 1292 | do { 1293 | let publisher = Fail(error: TestError()) 1294 | let recorder = publisher.record() 1295 | try wait(for: recorder.finished, timeout: 1) 1296 | XCTFail("Expected TestError") 1297 | } catch is TestError { } 1298 | do { 1299 | let publisher = (0..<1).publisher.append(error: TestError()) 1300 | let recorder = publisher.record() 1301 | try wait(for: recorder.finished, timeout: 1) 1302 | XCTFail("Expected TestError") 1303 | } catch is TestError { } 1304 | do { 1305 | let publisher = (0..<2).publisher.append(error: TestError()) 1306 | let recorder = publisher.record() 1307 | try wait(for: recorder.finished, timeout: 1) 1308 | XCTFail("Expected TestError") 1309 | } catch is TestError { } 1310 | } 1311 | 1312 | func testWaitForFinishedFailureAsync() throws { 1313 | do { 1314 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 1315 | let recorder = publisher.record() 1316 | try wait(for: recorder.finished, timeout: 1) 1317 | XCTFail("Expected TestError") 1318 | } catch is TestError { } 1319 | do { 1320 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1321 | let recorder = publisher.record() 1322 | try wait(for: recorder.finished, timeout: 1) 1323 | XCTFail("Expected TestError") 1324 | } catch is TestError { } 1325 | do { 1326 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1327 | let recorder = publisher.record() 1328 | try wait(for: recorder.finished, timeout: 1) 1329 | XCTFail("Expected TestError") 1330 | } catch is TestError { } 1331 | } 1332 | 1333 | func testWaitForFinishedInverted() throws { 1334 | do { 1335 | let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) 1336 | let recorder = publisher.record() 1337 | try wait(for: recorder.finished.inverted, timeout: 0.01) 1338 | } 1339 | do { 1340 | let publisher = PassthroughSubject() 1341 | let recorder = publisher.record() 1342 | try wait(for: recorder.finished.inverted, timeout: 0.01) 1343 | publisher.send(completion: .finished) 1344 | try wait(for: recorder.finished, timeout: 0.01) 1345 | } 1346 | } 1347 | 1348 | // MARK: - wait(for: recorder.completion) 1349 | 1350 | func testWaitForCompletionSync() throws { 1351 | do { 1352 | let publisher = Empty() 1353 | let recorder = publisher.record() 1354 | let completion = try wait(for: recorder.completion, timeout: 1) 1355 | if case let .failure(error) = completion { throw error } 1356 | } 1357 | do { 1358 | let publisher = (0..<1).publisher 1359 | let recorder = publisher.record() 1360 | let completion = try wait(for: recorder.completion, timeout: 1) 1361 | if case let .failure(error) = completion { throw error } 1362 | } 1363 | do { 1364 | let publisher = (0..<2).publisher 1365 | let recorder = publisher.record() 1366 | let completion = try wait(for: recorder.completion, timeout: 1) 1367 | if case let .failure(error) = completion { throw error } 1368 | } 1369 | } 1370 | 1371 | func testWaitForCompletionAsync() throws { 1372 | do { 1373 | let publisher = Empty().receive(on: DispatchQueue.main) 1374 | let recorder = publisher.record() 1375 | let completion = try wait(for: recorder.completion, timeout: 1) 1376 | if case let .failure(error) = completion { throw error } 1377 | } 1378 | do { 1379 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 1380 | let recorder = publisher.record() 1381 | let completion = try wait(for: recorder.completion, timeout: 1) 1382 | if case let .failure(error) = completion { throw error } 1383 | } 1384 | do { 1385 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 1386 | let recorder = publisher.record() 1387 | let completion = try wait(for: recorder.completion, timeout: 1) 1388 | if case let .failure(error) = completion { throw error } 1389 | } 1390 | } 1391 | 1392 | func testWaitForCompletionFailure() throws { 1393 | do { 1394 | let publisher = Fail(error: TestError()) 1395 | let recorder = publisher.record() 1396 | let completion = try wait(for: recorder.completion, timeout: 1) 1397 | if case .finished = completion { 1398 | XCTFail("Expected TestError") 1399 | } 1400 | } 1401 | do { 1402 | let publisher = (0..<1).publisher.append(error: TestError()) 1403 | let recorder = publisher.record() 1404 | let completion = try wait(for: recorder.completion, timeout: 1) 1405 | if case .finished = completion { 1406 | XCTFail("Expected TestError") 1407 | } 1408 | } 1409 | do { 1410 | let publisher = (0..<2).publisher.append(error: TestError()) 1411 | let recorder = publisher.record() 1412 | let completion = try wait(for: recorder.completion, timeout: 1) 1413 | if case .finished = completion { 1414 | XCTFail("Expected TestError") 1415 | } 1416 | } 1417 | } 1418 | 1419 | func testWaitForCompletionFailureAsync() throws { 1420 | do { 1421 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 1422 | let recorder = publisher.record() 1423 | let completion = try wait(for: recorder.completion, timeout: 1) 1424 | if case .finished = completion { 1425 | XCTFail("Expected TestError") 1426 | } 1427 | } 1428 | do { 1429 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1430 | let recorder = publisher.record() 1431 | let completion = try wait(for: recorder.completion, timeout: 1) 1432 | if case .finished = completion { 1433 | XCTFail("Expected TestError") 1434 | } 1435 | } 1436 | do { 1437 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1438 | let recorder = publisher.record() 1439 | let completion = try wait(for: recorder.completion, timeout: 1) 1440 | if case .finished = completion { 1441 | XCTFail("Expected TestError") 1442 | } 1443 | } 1444 | } 1445 | 1446 | // MARK: - wait(for: recorder.recording) 1447 | 1448 | func testWaitForRecordingSync() throws { 1449 | do { 1450 | let publisher = Empty() 1451 | let recorder = publisher.record() 1452 | let recording = try wait(for: recorder.recording, timeout: 1) 1453 | XCTAssertEqual(recording.output, []) 1454 | if case let .failure(error) = recording.completion { 1455 | XCTFail("Unexpected error \(error)") 1456 | } 1457 | } 1458 | do { 1459 | let publisher = (0..<1).publisher 1460 | let recorder = publisher.record() 1461 | let recording = try wait(for: recorder.recording, timeout: 1) 1462 | XCTAssertEqual(recording.output, [0]) 1463 | if case let .failure(error) = recording.completion { 1464 | XCTFail("Unexpected error \(error)") 1465 | } 1466 | } 1467 | do { 1468 | let publisher = (0..<2).publisher 1469 | let recorder = publisher.record() 1470 | let recording = try wait(for: recorder.recording, timeout: 1) 1471 | XCTAssertEqual(recording.output, [0, 1]) 1472 | if case let .failure(error) = recording.completion { 1473 | XCTFail("Unexpected error \(error)") 1474 | } 1475 | } 1476 | } 1477 | 1478 | func testWaitForRecordingAsync() throws { 1479 | do { 1480 | let publisher = Empty().receive(on: DispatchQueue.main) 1481 | let recorder = publisher.record() 1482 | let recording = try wait(for: recorder.recording, timeout: 1) 1483 | XCTAssertEqual(recording.output, []) 1484 | if case let .failure(error) = recording.completion { 1485 | XCTFail("Unexpected error \(error)") 1486 | } 1487 | } 1488 | do { 1489 | let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) 1490 | let recorder = publisher.record() 1491 | let recording = try wait(for: recorder.recording, timeout: 1) 1492 | XCTAssertEqual(recording.output, [0]) 1493 | if case let .failure(error) = recording.completion { 1494 | XCTFail("Unexpected error \(error)") 1495 | } 1496 | } 1497 | do { 1498 | let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) 1499 | let recorder = publisher.record() 1500 | let recording = try wait(for: recorder.recording, timeout: 1) 1501 | XCTAssertEqual(recording.output, [0, 1]) 1502 | if case let .failure(error) = recording.completion { 1503 | XCTFail("Unexpected error \(error)") 1504 | } 1505 | } 1506 | } 1507 | 1508 | func testWaitForRecordingFailure() throws { 1509 | do { 1510 | let publisher = Fail(error: TestError()) 1511 | let recorder = publisher.record() 1512 | let recording = try wait(for: recorder.recording, timeout: 1) 1513 | XCTAssertEqual(recording.output, []) 1514 | if case .finished = recording.completion { 1515 | XCTFail("Expected TestError") 1516 | } 1517 | } 1518 | do { 1519 | let publisher = (0..<1).publisher.append(error: TestError()) 1520 | let recorder = publisher.record() 1521 | let recording = try wait(for: recorder.recording, timeout: 1) 1522 | XCTAssertEqual(recording.output, [0]) 1523 | if case .finished = recording.completion { 1524 | XCTFail("Expected TestError") 1525 | } 1526 | } 1527 | do { 1528 | let publisher = (0..<2).publisher.append(error: TestError()) 1529 | let recorder = publisher.record() 1530 | let recording = try wait(for: recorder.recording, timeout: 1) 1531 | XCTAssertEqual(recording.output, [0, 1]) 1532 | if case .finished = recording.completion { 1533 | XCTFail("Expected TestError") 1534 | } 1535 | } 1536 | } 1537 | 1538 | func testWaitForRecordingFailureAsync() throws { 1539 | do { 1540 | let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) 1541 | let recorder = publisher.record() 1542 | let recording = try wait(for: recorder.recording, timeout: 1) 1543 | XCTAssertEqual(recording.output, []) 1544 | if case .finished = recording.completion { 1545 | XCTFail("Expected TestError") 1546 | } 1547 | } 1548 | do { 1549 | let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1550 | let recorder = publisher.record() 1551 | let recording = try wait(for: recorder.recording, timeout: 1) 1552 | XCTAssertEqual(recording.output, [0]) 1553 | if case .finished = recording.completion { 1554 | XCTFail("Expected TestError") 1555 | } 1556 | } 1557 | do { 1558 | let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) 1559 | let recorder = publisher.record() 1560 | let recording = try wait(for: recorder.recording, timeout: 1) 1561 | XCTAssertEqual(recording.output, [0, 1]) 1562 | if case .finished = recording.completion { 1563 | XCTFail("Expected TestError") 1564 | } 1565 | } 1566 | } 1567 | } 1568 | -------------------------------------------------------------------------------- /Tests/CombineExpectationsTests/Support.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | 3 | extension Publisher where Failure == Never { 4 | /// Returns a publisher which completes with an error. 5 | func append(error: Failure) -> AnyPublisher { 6 | setFailureType(to: Failure.self) 7 | .append(Fail(error: error)) 8 | .eraseToAnyPublisher() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Tests/CombineExpectationsTests/WackySubscriberTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Combine 3 | import Foundation 4 | import CombineExpectations 5 | 6 | /// Tests that Recorder fail tests when they are fed with a subscriber that does 7 | /// not behave correctly, and messes with the Recorder state machine. 8 | /// 9 | /// Our goal is to make it clear that the problem with wacky publishers is 10 | /// wacky publishers, not this library. 11 | class WackySubscriberTests: FailureTestCase { 12 | func testDoubleSubscriptionPublisher() throws { 13 | struct DoubleSubscriptionPublisher: Publisher { 14 | typealias Output = Base.Output 15 | typealias Failure = Base.Failure 16 | let base: Base 17 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 18 | base.receive(subscriber: subscriber) 19 | base.receive(subscriber: subscriber) 20 | } 21 | } 22 | assertFailure("failed - Publisher recorder is already subscribed") { 23 | let publisher = DoubleSubscriptionPublisher(base: Just("foo").makeConnectable()) 24 | _ = publisher.record() 25 | } 26 | } 27 | 28 | func testCompletionBeforeSubscriptionPublisher() throws { 29 | struct CompletionBeforeSubscriptionPublisher: Publisher { 30 | typealias Output = Never 31 | typealias Failure = Never 32 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 33 | subscriber.receive(completion: .finished) 34 | } 35 | } 36 | assertFailure("failed - Publisher recorder got unexpected completion before subscription: finished") { 37 | let publisher = CompletionBeforeSubscriptionPublisher() 38 | _ = publisher.record() 39 | } 40 | } 41 | 42 | func testInputBeforeSubscriptionPublisher() throws { 43 | struct InputBeforeSubscriptionPublisher: Publisher { 44 | typealias Output = String 45 | typealias Failure = Never 46 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 47 | _ = subscriber.receive("foo") 48 | } 49 | } 50 | assertFailure(#"failed - Publisher recorder got unexpected input before subscription: "foo""#) { 51 | let publisher = InputBeforeSubscriptionPublisher() 52 | _ = publisher.record() 53 | } 54 | } 55 | 56 | func testInputAfterCompletionPublisher() throws { 57 | struct InputAfterCompletionPublisher: Publisher 58 | where Base.Output == String 59 | { 60 | typealias Output = Base.Output 61 | typealias Failure = Base.Failure 62 | let base: Base 63 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 64 | base.receive(subscriber: subscriber) 65 | _ = subscriber.receive("bar") 66 | } 67 | } 68 | assertFailure(#"failed - Publisher recorder got unexpected input after completion: "bar""#) { 69 | let publisher = InputAfterCompletionPublisher(base: Just("foo")) 70 | _ = publisher.record() 71 | } 72 | } 73 | 74 | func testDoubleCompletionPublisher() throws { 75 | struct DoubleCompletionPublisher: Publisher { 76 | typealias Output = Base.Output 77 | typealias Failure = Base.Failure 78 | let base: Base 79 | func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { 80 | base.receive(subscriber: subscriber) 81 | subscriber.receive(completion: .finished) 82 | } 83 | } 84 | assertFailure("failed - Publisher recorder got unexpected completion after completion") { 85 | let publisher = DoubleCompletionPublisher(base: Just("foo")) 86 | _ = publisher.record() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/CombineExpectationsTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(CombineExpectationsTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CombineExpectationsTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CombineExpectationsTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------