├── .gitignore ├── .swift-version ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── RxNimble.podspec ├── Sources └── RxNimble │ ├── Core │ └── Expectation+Ext.swift │ ├── RxBlocking │ └── Expectation+Blocking.swift │ ├── RxNimbleRxBlocking.h │ ├── RxNimbleRxTest.h │ └── RxTest │ ├── Equal+RxTest.swift │ ├── Expectation+RxTest.swift │ └── ThrowError+RxTest.swift └── Tests └── RxNimbleTests ├── AnyError.swift ├── RxNimbleRxBlockingTests.swift └── RxNimbleRxTestTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | Pods/ 27 | 28 | # Carthage 29 | # 30 | Carthage 31 | 32 | # AppCode 33 | 34 | .idea 35 | 36 | .swiftpm 37 | 38 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Current Master 4 | 5 | - Nothing yet. 6 | 7 | ## 6.3.1 8 | 9 | - Rename `Predicate` to `Matcher` to support Xcode 15. 10 | 11 | ## 6.3.0 12 | 13 | - Updates to Nimble 13. See [#69](https://github.com/RxSwiftCommunity/RxNimble/issues/69). 14 | 15 | ## 6.2.0 16 | 17 | - Support for new Nimble 12 dependency. See [#68](https://github.com/RxSwiftCommunity/RxNimble/pull/68). 18 | - Remove Carthage dependency and build steps. 19 | - Move to Swift Package Manager instead of xcodeproj. 20 | - Reorganized file structure to SPM standard. 21 | 22 | ## 6.1.0 23 | 24 | - Support for new Nimble 11 dependency. See [#67](https://github.com/RxSwiftCommunity/RxNimble/pull/67). 25 | 26 | ## 6.0.0 27 | 28 | - Support for new Nimble 10 dependency. See [#66](https://github.com/RxSwiftCommunity/RxNimble/pull/66). 29 | 30 | ## 5.1.2 31 | 32 | - Updates Nimble version (see [#65](https://github.com/RxSwiftCommunity/RxNimble/pull/65)). 33 | 34 | ## 5.1.1 35 | 36 | - Updates minimum deployment targets to iOS 9 and macOS 10.12. 37 | 38 | ## 5.1.0 39 | 40 | - Support [RxSwift 6](https://dev.to/freak4pc/what-s-new-in-rxswift-6-2nog) 41 | - Adds a [carthage.sh](https://github.com/Carthage/Carthage/issues/3019#issuecomment-665136323) script to workaround a Carthage issue 42 | 43 | ## 5.0.0 44 | 45 | - Support for new Nimble 9 dependency. See [#59](https://github.com/RxSwiftCommunity/RxNimble/pull/59). 46 | - Increased minimum iOS deployment target to 9.0. 47 | 48 | ## 4.7.2 49 | 50 | - More weak linking fixus. A Carthage/SPM-only release. 51 | 52 | ## 4.7.1 53 | 54 | - Weak links with XCTest. 55 | 56 | ## 4.7.0 57 | 58 | - Support for Swift Package Manager 59 | - Expectation+Blocking.swift imports `Foundation` to fix a compilation issue with SPM 60 | 61 | ## 4.6.0 62 | 63 | - Support for RxSwift traits 64 | - Adds support for passing a timeout to RxBlockings' operators 65 | 66 | ## 4.5.0 67 | 68 | - Carthage support 69 | - Support for Xcode 10.2, RxSwift 5.0 & Swift 5 70 | 71 | ## 4.4.1 72 | 73 | - Adds support for Carthage (4.4.1 does not exist on CocoaPods). 74 | 75 | ## 4.4.0 76 | 77 | - Added support to RxTest. Users may now choose between `RxTest` and `RxBlocking` (or both) 78 | 79 | ## 4.3.0 80 | 81 | - Swift 4.2 support 82 | - Xcode 10 support 83 | - Update Demo supporting Xcode 10 with Swift 4.2 84 | 85 | ## 4.2.0 86 | 87 | - Updated RxSwift requirement. 88 | - Swift 4.1 support again. See [#30](https://github.com/RxSwiftCommunity/RxNimble/pull/31). 89 | 90 | ## 4.1.1 91 | 92 | - Swift 4.1 support. See [#30](https://github.com/RxSwiftCommunity/RxNimble/issues/30). 93 | 94 | ## 4.1.0 95 | 96 | - Deprecate old matcher functions 97 | - Introduce following properties for `expect(observable)` to match `RxBlocking` operator 98 | - `first` 99 | - `last` 100 | - `array` 101 | 102 | ## 4.0.0 103 | 104 | - Support for Swift 4.0, RxSwift 4.0 and RxBlocking 4.0. See [#27](https://github.com/RxSwiftCommunity/RxNimble/pull/27). 105 | 106 | ## 3.0.0 107 | 108 | - Support for new Nimble 7 api. See [#21](https://github.com/RxSwiftCommunity/RxNimble/pull/21). 109 | 110 | ## 1.0.0 111 | 112 | - Swift 3 support. 113 | 114 | ## 0.2.0 115 | 116 | - Fixes build issues. See [#8](https://github.com/RxSwiftCommunity/RxNimble/pull/8). 117 | 118 | ## 0.1.0 119 | 120 | - Initial release. 121 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Preparing for development 2 | 3 | 1. Check out this repository. 4 | 2. Open `Package.swift` and start hacking! 5 | 6 | ## Submitting a Pull Request 7 | 8 | When submitting new code, please make sure that it is backed by tests. 9 | 10 | ## Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | - (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | - (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | - (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | - (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. 35 | 36 | *Wording of statement copied from [elinux.org](http://elinux.org/Developer_Certificate_Of_Origin)* 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ash Furrow 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "cwlcatchexception", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mattgallagher/CwlCatchException.git", 7 | "state" : { 8 | "revision" : "35f9e770f54ce62dd8526470f14c6e137cef3eea", 9 | "version" : "2.1.1" 10 | } 11 | }, 12 | { 13 | "identity" : "cwlpreconditiontesting", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", 16 | "state" : { 17 | "revision" : "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", 18 | "version" : "2.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "nimble", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/Quick/Nimble", 25 | "state" : { 26 | "revision" : "d616f15123bfb36db1b1075153f73cf40605b39d", 27 | "version" : "13.0.0" 28 | } 29 | }, 30 | { 31 | "identity" : "quick", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/Quick/Quick", 34 | "state" : { 35 | "revision" : "16910e406be96e08923918315388c3e989deac9e", 36 | "version" : "6.1.0" 37 | } 38 | }, 39 | { 40 | "identity" : "rxswift", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/ReactiveX/RxSwift", 43 | "state" : { 44 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", 45 | "version" : "6.5.0" 46 | } 47 | } 48 | ], 49 | "version" : 2 50 | } 51 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7.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: "RxNimble", 8 | platforms: [ 9 | .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macOS(.v10_15) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 13 | .library(name: "RxNimble", targets: ["RxNimble"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | .package(url: "https://github.com/ReactiveX/RxSwift", .upToNextMajor(from: "6.0.0")), 18 | .package(url: "https://github.com/Quick/Nimble", .upToNextMajor(from: "13.0.0")), 19 | .package(url: "https://github.com/Quick/Quick", .upToNextMajor(from: "6.1.0")), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "RxNimble", 26 | dependencies: [ 27 | "RxSwift", 28 | "Nimble", 29 | .product(name: "RxTest", package: "RxSwift"), 30 | .product(name: "RxBlocking", package: "RxSwift") 31 | ] 32 | ), 33 | .testTarget( 34 | name: "RxNimbleTests", 35 | dependencies: [ 36 | "RxNimble", 37 | "Quick", 38 | ] 39 | ), 40 | ], 41 | swiftLanguageVersions: [.v5] 42 | ) 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/RxSwiftCommunity/RxNimble.svg?branch=master)](https://travis-ci.org/RxSwiftCommunity/RxNimble) 2 | 3 | # RxNimble 4 | 5 | Nimble extensions that make unit testing with RxSwift easier :tada: 6 | 7 | If you came here because you want to help out, please check out the [contribution guide](CONTRIBUTING.md) 8 | 9 | ## Why 10 | 11 | RxSwift includes a really nifty little library called [RxBlocking](http://cocoapods.org/pods/RxBlocking) which provides convenience functions for peeking in on `Observable` instances. Check is a *blocking call*, hence the name. 12 | 13 | But writing code to check an `Observable`'s value is sooooo tedious: 14 | 15 | ```swift 16 | let result = try! observable.toBlocking().first() 17 | expect(result) == 42 18 | ``` 19 | 20 | With `RxNimble`, we've added [Nimble](https://github.com/Quick/Nimble) extension for `Observable`s, so the code above can be rewritten as: 21 | 22 | ```swift 23 | expect(observable).first == 42 24 | ``` 25 | 26 | Nice. 27 | 28 | It's also possible to pass a timeout to the blocking operators: 29 | 30 | ```swift 31 | expect(observable).first(timeout: 3) == 42 32 | ``` 33 | 34 | This extension is also available for all Traits (e.g. `Single`, `Maybe`) and other types conforming to `ObservableConvertibleType`. 35 | 36 | --- 37 | 38 | If on the other hand you'd rather use [RxTest](http://cocoapods.org/pods/RxTest) instead of `RxBlocking`, you can do it by specifying RxNimble's `RxTest` subspec. With _RxTest_ you can have more powerful tests, checking a stream as a whole instead of being limited to `first`, `last` and `array` (while the last 2 implicitly require the stream to have completed). 39 | 40 | That means _RxTest_ allows you to verify the occurrence of multiple `next`, `error` and `completed` events at specific virtual times: 41 | 42 | ```swift 43 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag) 44 | .to(equal([ 45 | Recorded.next(5, "Hello"), 46 | Recorded.next(10, "World"), 47 | Recorded.completed(100) 48 | ])) 49 | ``` 50 | 51 | You may also verify specific error types: 52 | 53 | ```swift 54 | expect(imageSubject).events(scheduler: scheduler, disposeBag: disposeBag) 55 | .to(equal([ 56 | Recorded.error(5, ImageError.invalidImage) 57 | ])) 58 | ``` 59 | 60 | ## Installation 61 | 62 | ### CocoaPods 63 | 64 | Add to the tests target in your Podfile: 65 | 66 | ```rb 67 | pod 'RxNimble' # same as RxNimble/RxBlocking 68 | ``` 69 | 70 | or 71 | 72 | ```rb 73 | pod 'RxNimble/RxTest' # installs RxTest instead of RxBlocking 74 | ``` 75 | 76 | or even 77 | 78 | ```rb 79 | pod 'RxNimble', subspecs: ['RxBlocking', 'RxTest'] # installs both dependencies 80 | ``` 81 | 82 | And `pod install` and that's it! 83 | 84 | ### Carthage 85 | 86 | Add to your Cartfile.private: 87 | 88 | ```rb 89 | github 'RxSwiftCommunity/RxNimble' 90 | ``` 91 | 92 | Run `carthage update --cache-builds` then drag & drop from the Carthage/Builds folder into your project either or both of: 93 | 94 | - `RxNimbleRxBlocking.framework` and `RxBlocking.framework` 95 | - `RxNimbleRxTest.framework` and `RxTest.framework` 96 | 97 | ## Migration 4.5.0 -> 5.0.0 98 | 99 | Deprecated function `equalFirst` was removed in favor of a more natural Nimble matcher API style. 100 | 101 | ### RxNimble 4.5.0: 102 | ```swift 103 | expect(o).to(equalFirst(...)) 104 | ``` 105 | ### RxNimble 5.0.0: 106 | ```swift 107 | expect(o).first.to(equal(...)) 108 | ``` 109 | 110 | ## Known Issues 111 | 112 | Very very _very_ rarely the Swift compiler gets confused about the different types and you need to use the original `RxBlocking` code. 113 | 114 | ## License 115 | 116 | MIT ofc. 117 | 118 | ![Give yourself a high five](https://media.giphy.com/media/dRkMyTvCuAdY4/giphy.gif) 119 | -------------------------------------------------------------------------------- /RxNimble.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "RxNimble" 3 | s.version = "6.3.1" 4 | s.summary = "Nimble extensions that making unit testing with RxSwift easier 🎉" 5 | s.description = <<-DESC 6 | This library includes functions that make testing RxSwift projects easier with Nimble. 7 | DESC 8 | s.homepage = "https://github.com/RxSwiftCommunity/RxNimble" 9 | s.license = { :type => "MIT", :file => "LICENSE" } 10 | s.author = { "RxSwiftCommunity" => "https://github.com/RxSwiftCommunity" } 11 | 12 | s.ios.deployment_target = "13.0" 13 | s.osx.deployment_target = "10.15" 14 | s.tvos.deployment_target = "13.0" 15 | s.source = { :git => "https://github.com/RxSwiftCommunity/RxNimble.git", :tag => s.version } 16 | s.default_subspec = "RxBlocking" 17 | s.frameworks = "Foundation", "XCTest" 18 | 19 | s.pod_target_xcconfig = { 'ENABLE_BITCODE' => 'NO', 'FRAMEWORK_SEARCH_PATHS' => '$(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks"' } 20 | 21 | s.subspec "Core" do |ss| 22 | ss.source_files = "Sources/RxNimble/Core/" 23 | ss.dependency "Nimble", "~> 13.0" 24 | ss.dependency "RxSwift", "~> 6.0" 25 | end 26 | 27 | s.subspec "RxBlocking" do |ss| 28 | ss.source_files = "Sources/RxNimble/RxBlocking/" 29 | ss.dependency "RxNimble/Core" 30 | ss.dependency "RxBlocking" 31 | end 32 | 33 | s.subspec "RxTest" do |ss| 34 | ss.source_files = "Sources/RxNimble/RxTest/" 35 | ss.dependency "RxNimble/Core" 36 | ss.dependency "RxTest" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /Sources/RxNimble/Core/Expectation+Ext.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | 3 | extension SyncExpectation { 4 | #if swift(>=4.1) 5 | #else 6 | init(_ expression: SyncExpectation) { 7 | self.expression = expression 8 | } 9 | #endif 10 | 11 | internal func transform(_ closure: @escaping (Value?) throws -> U?) -> SyncExpectation{ 12 | let exp = expression.cast(closure) 13 | #if swift(>=4.1) 14 | return SyncExpectation(expression: exp) 15 | #else 16 | return SyncExpectation(exp) 17 | #endif 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/RxNimble/RxBlocking/Expectation+Blocking.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Expectation+Blocking.swift 3 | // Pods 4 | // 5 | // Created by Mostafa Amer on 11.05.17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Nimble 11 | import RxSwift 12 | import RxBlocking 13 | 14 | public extension SyncExpectation where Value: ObservableConvertibleType { 15 | 16 | // MARK: - first 17 | 18 | /// Expectation with sequence's first element 19 | /// 20 | /// Transforms the expression by blocking sequence and returns its first element. 21 | /// - parameter timeout: Maximal time interval waiting for first element to emit before throwing error 22 | func first(timeout: TimeInterval? = nil) -> SyncExpectation { 23 | return transform { source in 24 | try source?.toBlocking(timeout: timeout).first() 25 | } 26 | } 27 | 28 | /// Expectation with sequence's first element 29 | /// 30 | /// Transforms the expression by blocking sequence and returns its first element. 31 | var first: SyncExpectation { 32 | return first() 33 | } 34 | 35 | // MARK: - last 36 | 37 | /// Expectation with sequence's last element 38 | /// 39 | /// Transforms the expression by blocking sequence and returns its last element. 40 | /// - parameter timeout: Maximal time interval waiting for sequence to complete before throwing error 41 | func last(timeout: TimeInterval? = nil) -> SyncExpectation { 42 | return transform { source in 43 | try source?.toBlocking(timeout: timeout).last() 44 | } 45 | } 46 | 47 | /// Expectation with sequence's last element 48 | /// 49 | /// Transforms the expression by blocking sequence and returns its last element. 50 | var last: SyncExpectation { 51 | return last() 52 | } 53 | 54 | // MARK: - array 55 | 56 | /// Expectation with all sequence's elements 57 | /// 58 | /// Transforms the expression by blocking sequence and returns its elements. 59 | /// - parameter timeout: Maximal time interval waiting for sequence to complete before throwing error 60 | func array(timeout: TimeInterval? = nil) -> SyncExpectation<[Value.Element]> { 61 | return transform { source in 62 | try source?.toBlocking(timeout: timeout).toArray() 63 | } 64 | } 65 | 66 | /// Expectation with all sequence's elements 67 | /// 68 | /// Transforms the expression by blocking sequence and returns its elements. 69 | var array: SyncExpectation<[Value.Element]> { 70 | return array() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/RxNimble/RxNimbleRxBlocking.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for RxNimbleRxBlocking. 4 | FOUNDATION_EXPORT double RxNimbleRxBlockingVersionNumber; 5 | 6 | //! Project version string for RxNimbleRxBlocking. 7 | FOUNDATION_EXPORT const unsigned char RxNimbleRxBlockingVersionString[]; 8 | -------------------------------------------------------------------------------- /Sources/RxNimble/RxNimbleRxTest.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for RxNimbleRxTest. 4 | FOUNDATION_EXPORT double RxNimbleRxTestVersionNumber; 5 | 6 | //! Project version string for RxNimbleRxTest. 7 | FOUNDATION_EXPORT const unsigned char RxNimbleRxTestVersionString[]; 8 | -------------------------------------------------------------------------------- /Sources/RxNimble/RxTest/Equal+RxTest.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import RxSwift 3 | import RxTest 4 | 5 | /// A Nimble matcher that succeeds when the actual events are equal to the expected events. 6 | public func equal(_ expectedEvents: RecordedEvents) -> Matcher> { 7 | Matcher.define { actualEvents in 8 | let actualEquatableEvents = try actualEvents.evaluate()?.map { AnyEquatable(target: $0, comparer: ==) } 9 | let expectedEquatableEvents = expectedEvents.map { AnyEquatable(target: $0, comparer: ==) } 10 | 11 | let matches = (actualEquatableEvents == expectedEquatableEvents) 12 | return MatcherResult(bool: matches, 13 | message: .expectedActualValueTo( 14 | "emit <\(stringify(expectedEquatableEvents))>") 15 | ) 16 | } 17 | } 18 | 19 | // Borrowed this implementation from RxTest. 20 | struct AnyEquatable 21 | : Equatable { 22 | typealias Comparer = (Target, Target) -> Bool 23 | 24 | let _target: Target 25 | let _comparer: Comparer 26 | 27 | init(target: Target, comparer: @escaping Comparer) { 28 | _target = target 29 | _comparer = comparer 30 | } 31 | } 32 | 33 | func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool { 34 | return lhs._comparer(lhs._target, rhs._target) 35 | } 36 | -------------------------------------------------------------------------------- /Sources/RxNimble/RxTest/Expectation+RxTest.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import RxSwift 3 | import RxTest 4 | 5 | public typealias RecordedEvents = [Recorded>] 6 | 7 | public extension SyncExpectation where Value: ObservableConvertibleType { 8 | /// Make an expectation on the events emitted by an observable. 9 | /// 10 | /// - Parameters: 11 | /// - scheduler: the scheduler used to record events in virtual time units. 12 | /// - disposeBag: the dispose bag that will dispose all of its resources between tests. 13 | /// - initialTime: the time at which subscription/recording should begin. 14 | /// - Returns: an expectation of the actual events emitted by the observable. 15 | func events(scheduler: TestScheduler, 16 | disposeBag: DisposeBag, 17 | startAt initialTime: Int = 0) -> SyncExpectation> { 18 | return transform { source in 19 | let results = scheduler.createObserver(Value.Element.self) 20 | 21 | scheduler.scheduleAt(initialTime) { 22 | source?.asObservable().subscribe(results).disposed(by: disposeBag) 23 | } 24 | scheduler.start() 25 | 26 | return results.events 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/RxNimble/RxTest/ThrowError+RxTest.swift: -------------------------------------------------------------------------------- 1 | import Nimble 2 | import RxSwift 3 | import RxTest 4 | 5 | /// A Nimble matcher that succeeds when the actual events emit an error 6 | /// of any type. 7 | public func throwError() -> Matcher> { 8 | func extractError(_ recorded: RecordedEvents?) -> [Error]? { 9 | func extractError(_ recorded: Recorded>) -> Error? { 10 | return recorded.value.error 11 | } 12 | 13 | #if swift(>=4.1) 14 | return recorded?.compactMap(extractError) 15 | #else 16 | return recorded?.flatMap(extractError) 17 | #endif 18 | } 19 | 20 | 21 | return Matcher { actualEvents in 22 | var actualError: Error? 23 | do { 24 | let recordedEvents = try actualEvents.evaluate() 25 | if let error = extractError(recordedEvents)?.first { 26 | throw error 27 | } 28 | } catch { 29 | actualError = error 30 | } 31 | 32 | if let actualError = actualError { 33 | return MatcherResult(bool: true, message: .expectedCustomValueTo("throw any error", actual: "<\(actualError)>")) 34 | } else { 35 | return MatcherResult(bool: false, message: .expectedCustomValueTo("throw any error", actual: "no error")) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/RxNimbleTests/AnyError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum AnyError: Error { 4 | case any 5 | } 6 | -------------------------------------------------------------------------------- /Tests/RxNimbleTests/RxNimbleRxBlockingTests.swift: -------------------------------------------------------------------------------- 1 | import Quick 2 | import Nimble 3 | import RxSwift 4 | import RxNimble 5 | 6 | class RxNimbleRxBlockingTests: QuickSpec { 7 | override func spec() { 8 | 9 | // MARK: First 10 | 11 | describe("First") { 12 | it("works with plain observables") { 13 | let subject = ReplaySubject.createUnbounded() 14 | subject.onNext("Hi") 15 | 16 | expect(subject).first == "Hi" 17 | } 18 | 19 | it("only checks the first value") { 20 | let subject = ReplaySubject.createUnbounded() 21 | subject.onNext("Hi") 22 | subject.onNext("Hello") 23 | 24 | expect(subject).first == "Hi" 25 | } 26 | 27 | it("can use different matchers") { 28 | let subject = ReplaySubject.createUnbounded() 29 | subject.onNext("") 30 | 31 | expect(subject).first.to(beEmpty()) 32 | } 33 | 34 | it("get first error") { 35 | let subject = ReplaySubject.createUnbounded() 36 | subject.onError(AnyError.any) 37 | 38 | expect(subject).first.to(throwError()) 39 | } 40 | 41 | it("fails if first value is not being emitted in time") { 42 | let subject = ReplaySubject.createUnbounded() 43 | 44 | // Never push onNext 45 | 46 | expect(subject).first(timeout: 1).to(throwError(RxError.timeout)) 47 | } 48 | } 49 | 50 | // MARK: Last 51 | 52 | describe("Last") { 53 | it("checks for last element") { 54 | let subject = ReplaySubject.createUnbounded() 55 | subject.onNext("Hello") 56 | subject.onNext("World") 57 | subject.onCompleted() 58 | 59 | expect(subject).last == "World" 60 | } 61 | 62 | it("is nil, if sequence is empty") { 63 | let subject = ReplaySubject.createUnbounded() 64 | subject.onCompleted() 65 | 66 | expect(subject).last.to(beNil()) 67 | } 68 | 69 | it("error, if terminated with error") { 70 | let subject = ReplaySubject.createUnbounded() 71 | subject.onNext("Hello, world!") 72 | subject.onError(AnyError.any) 73 | 74 | expect(subject).last.to(throwError()) 75 | } 76 | 77 | it("fails if last value is not being emitted in time") { 78 | let subject = ReplaySubject.createUnbounded() 79 | 80 | subject.onNext("Hi") 81 | // Never complete 82 | 83 | expect(subject).last(timeout: 1).to(throwError(RxError.timeout)) 84 | } 85 | } 86 | 87 | // MARK: Array 88 | 89 | describe("Array") { 90 | it("checks for timeline") { 91 | let subject = ReplaySubject.createUnbounded() 92 | subject.onNext("Hello") 93 | subject.onNext("World") 94 | subject.onCompleted() 95 | 96 | expect(subject).array == ["Hello", "World"] 97 | } 98 | } 99 | 100 | describe("Traits") { 101 | let expectedValue = "some" 102 | 103 | it("works with Single") { 104 | expect(Single.just(expectedValue)).first == expectedValue 105 | } 106 | 107 | it("works with Maybe") { 108 | expect(Maybe.just(expectedValue)).first == expectedValue 109 | } 110 | 111 | it("works with Completable") { 112 | expect(Observable.just(expectedValue).ignoreElements()).array.to(beEmpty()) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/RxNimbleTests/RxNimbleRxTestTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Quick 3 | import Nimble 4 | import RxSwift 5 | import RxTest 6 | import RxNimble 7 | 8 | class RxNimbleRxTestTests: QuickSpec { 9 | override func spec() { 10 | describe("Events") { 11 | let initialClock = 0 12 | let expectedValue = "some" 13 | var scheduler: TestScheduler! 14 | var disposeBag: DisposeBag! 15 | 16 | beforeEach { 17 | disposeBag = DisposeBag() 18 | DispatchQueue.main.async { 19 | scheduler = TestScheduler(initialClock: initialClock, simulateProcessingDelay: false) 20 | } 21 | } 22 | 23 | it("works with uncompleted streams") { 24 | DispatchQueue.main.async { 25 | let subject = scheduler.createHotObservable([ 26 | .next(5, "Hello"), 27 | .next(10, "World"), 28 | ]) 29 | 30 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag) 31 | .to(equal([ 32 | .next(5, "Hello"), 33 | .next(10, "World") 34 | ])) 35 | } 36 | } 37 | 38 | it("works with completed streams") { 39 | DispatchQueue.main.async { 40 | let subject = scheduler.createHotObservable([ 41 | .next(5, "Hello"), 42 | .next(10, "World"), 43 | .completed(100) 44 | ]) 45 | 46 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag) 47 | .to(equal([ 48 | .next(5, "Hello"), 49 | .next(10, "World"), 50 | .completed(100) 51 | ])) 52 | } 53 | } 54 | 55 | it("works with errored streams") { 56 | DispatchQueue.main.async { 57 | let subject: TestableObservable = scheduler.createHotObservable([ 58 | .error(5, AnyError.any) 59 | ]) 60 | 61 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag) 62 | .to(equal([ 63 | Recorded.error(5, AnyError.any) 64 | ])) 65 | } 66 | } 67 | 68 | it("throws error if any event is error") { 69 | DispatchQueue.main.async { 70 | let subject = scheduler.createHotObservable([ 71 | .next(5, "Hello"), 72 | .next(10, "World"), 73 | .error(15, AnyError.any) 74 | ]) 75 | 76 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag) 77 | .to(throwError()) 78 | } 79 | } 80 | 81 | it("does not throw error if no errors") { 82 | DispatchQueue.main.async { 83 | let subject = scheduler.createHotObservable([ 84 | .next(5, "Hello"), 85 | .next(10, "World") 86 | ]) 87 | 88 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag) 89 | .toNot(throwError()) 90 | } 91 | } 92 | 93 | it("subscribes at specified initial time") { 94 | DispatchQueue.main.async { 95 | let initialTime = 50 96 | let eventTime = 100 97 | let subject = scheduler.createColdObservable([ 98 | .next(eventTime, "Hi") 99 | ]) 100 | 101 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag, startAt: initialTime) 102 | .to(equal([ 103 | .next(initialTime + eventTime, "Hi") 104 | ])) 105 | } 106 | } 107 | 108 | it("ignores hot stream events before initial time") { 109 | DispatchQueue.main.async { 110 | let subject = scheduler.createHotObservable([ 111 | .next(5, "Hello"), 112 | .next(10, "World"), 113 | .completed(100) 114 | ]) 115 | 116 | expect(subject).events(scheduler: scheduler, disposeBag: disposeBag, startAt: 15) 117 | .to(equal([ 118 | .completed(100) 119 | ])) 120 | } 121 | } 122 | 123 | it("works with Single") { 124 | DispatchQueue.main.async { 125 | // Given 126 | let expectedEvents = Recorded.events(.next(5, expectedValue), .completed(5)) 127 | let single = scheduler.createHotObservable(expectedEvents).asSingle() 128 | // Then 129 | expect(single).events(scheduler: scheduler, disposeBag: disposeBag) == expectedEvents 130 | } 131 | } 132 | 133 | it("works with Maybe") { 134 | // Given 135 | DispatchQueue.main.async { 136 | let expectedEvents = Recorded.events(.next(5, expectedValue), .completed(5)) 137 | let maybe = scheduler.createHotObservable(expectedEvents).asMaybe() 138 | // Then 139 | expect(maybe).events(scheduler: scheduler, disposeBag: disposeBag) == expectedEvents 140 | } 141 | } 142 | 143 | it("works with Completable") { 144 | DispatchQueue.main.async { 145 | // Given 146 | let expectedEvents = Recorded>.events(.completed(5)) 147 | let completable = scheduler.createHotObservable(expectedEvents).ignoreElements() 148 | // Then 149 | expect(completable).events(scheduler: scheduler, disposeBag: disposeBag) == expectedEvents 150 | } 151 | } 152 | } 153 | } 154 | } 155 | --------------------------------------------------------------------------------