Skip to content

Commit

Permalink
Fix Method Overloading Error with .testable() (#7)
Browse files Browse the repository at this point in the history
* Add .testable()

* Fix image size

* Refix test

* Remove extra newline

* Readd comment

---------

Co-authored-by: Ethan van Heerden <[email protected]>
  • Loading branch information
ethan-vanheerden and Ethan van Heerden authored Dec 2, 2024
1 parent fad93ca commit 9ac94c5
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 171 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ Under the hood, `PublisherExpectation` is utilizing standard XCTest/Swift Testin

In an `XCTestCase` or a Swift Testing suite, add a new unit test function, as normal, preparing the `Publisher` test subject to be tested. Utilize any combination of the examples below to validate the behavior of any `Publisher` in your unit tests.

### A Note on Swift Testing

This library provides different targets, `TestableCombinePublishers` and `SwiftTestingTestableCombinePublishers`, which enable utilizing the convenience expectation testing functions in both an XCTest and Swift Testing test suite, respectively. In an effort to keep the call-site expectation function names the same across both targets, you need to add an additional `.testable()` call on your Publisher _before_ you can utilize any of the testing functions. Otherwise, the compiler will not know which library you are referring:

```swift
// Swift Testing only:
@Test func myPublisherTest() async {
await somePublisher
.testable() // Not needed in an XCTest suite
.expect(someEquatableValue)
// <other testing functions>
.waitForExpectations(timeout: 1)
}
```

### Examples

For a `Publisher` that is expected to emit a single value and complete with `.finished`
Expand All @@ -40,6 +55,7 @@ func testSingleValueCompletingPublisher() {
```swift
@Test func singleValueCompletingPublisher() async {
await somePublisher
.testable()
.expect(someEquatableValue)
.expectSuccess()
.waitForExpectations(timeout: 1)
Expand All @@ -64,6 +80,7 @@ func testMultipleValuePersistentPublisher() {
@Test func multipleValuePersistentPublisher() async {
await somePublisher
.collect(someCount)
.testable()
.expect(someEquatableValueArray)
.expectNoCompletion()
.waitForExpectations(timeout: 1)
Expand All @@ -85,6 +102,7 @@ func testPublisherFailure() {
```swift
@Test func publisherFailure() async {
await somePublisher
.testable()
.expectFailure()
.waitForExpectations(timeout: 1)
}
Expand All @@ -106,6 +124,7 @@ func testLoadablePublisher() {
```swift
@Test func loadablePublisher() async {
let test = someDataSource.publisher
.testable()
.expect(someEquatableValue)
someDataSource.load()
await test.waitForExpectations(timeout: 1)
Expand All @@ -131,6 +150,7 @@ func testNonEquatableSingleValue() {
```swift
@Test func nonEquatableSingleValue() async {
await somePublisher
.testable()
.expect({ value in
if case .loaded(let model) = value, !model.rows.isEmpty { } else {
Issue.record("Expected loaded and populated model")
Expand Down Expand Up @@ -162,6 +182,7 @@ func testNonEquatableFailure() {
```swift
@Test func nonEquatableFailure() async {
await somePublisher
.testable()
.expectFailure({ failure in
switch failure {
case .noInternet, .airPlaneMode:
Expand Down Expand Up @@ -212,7 +233,8 @@ It also negates the need to rely on custom `Equatable` implementations.

**Important Disclosures**

This is an imperfect and assuming implementation of `Equatable`. It should not be used without understanding the following concepts.
- This is an imperfect and assuming implementation of `Equatable`. It should not be used without understanding the following concepts.
- `AutomaticallyEquatable` is available in the `TestableCombinePublishersUtility` target

The implementation:

Expand Down Expand Up @@ -243,10 +265,17 @@ extension MyCustomType: AutomaticallyEquatable { /*no-op*/ }
Then, you can compare two of `MyCustomType` using `expect(...)`, `==`, or an XCTest/Swift Testing framework equality assertion.

```swift
// XCTest
somePublisher
.expect(MyCustomType.bar(Baz(answer: 42)))
.waitForExpectations(timeout: 1)

// Swift Testing
await somePublisher
.testable()
.expect(MyCustomType.bar(Baz(answer: 42)))
.waitForExpectations(timeout: 1)

// or

XCTAssertEqual(output, MyCustomType.bar(Baz(answer: 42))) // If using XCTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ import Testing
import Combine
import TestableCombinePublishersUtility

// MARK: - Swift Testing-Usable Publisher Extension

public extension Publisher {

/// Creates an explicit `SwiftTestingPublisherExpectation` whose upstream publisher refers to this `Publisher`.
/// This function is required to be called on a `Publisher` before any of the Swift Testing convenience testing functions can be used.
/// This method is required to avoid compiler confusion, as both the Swift Testing and XCTest versions of this library have the same function names.
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func testable() -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self)
}
}

// MARK: - SwiftTestingPublisherExpectation

/// Provides a convenient way for `Publisher`s to be unit tested.
/// To use this, you can start by typing `expect` on any `Publisher` type.
/// `waitForExpectations` must be called to evaluate the expectations.
Expand Down Expand Up @@ -231,73 +246,6 @@ public extension SwiftTestingPublisherExpectation {
}
}

// MARK: - Publisher Extension for Receive Value Expectations

public extension Publisher {
/// Asserts that the provided value will be emitted by the `Publisher`
/// - Parameters:
/// - expected: The `Equatable` value expected from the `Publisher`
/// - message: The message to attach to the `#expect` failure, if a mismatch is found
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expect(_ expected: Output, message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> where Output: Equatable {
.init(upstreamPublisher: self).expect(expected, message: message, sourceLocation: sourceLocation)
}

/// Asserts that the provided `Equatable` value will be emitted by the `Publisher` exactly `count` times.
/// ⚠️ This will wait for the full timeout.
/// - Parameters:
/// - count: The exact number of values that should be emitted from the `Publisher`
/// - expected: The `Equatable` value expected from the `Publisher`
/// - message: The message to attach to the `#expect` failure, if a mismatch is found
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectExactly(_ count: Int, of expected: Output, message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> where Output: Equatable {
.init(upstreamPublisher: self).expectExactly(count, of: expected, message: message, sourceLocation: sourceLocation)
}

/// Asserts that a value will be emitted by the `Publisher` and that it does NOT match the provided `Equatable`
/// - Parameters:
/// - expected: The `Equatable` value NOT expected from the `Publisher`
/// - message: The message to attach to the `#expect` failure, if a match is found
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectNot(_ expected: Output, message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> where Output: Equatable {
.init(upstreamPublisher: self).expectNot(expected, message: message, sourceLocation: sourceLocation)
}

/// Asserts that no value will be emitted by the `Publisher`.
/// ⚠️ This will wait for the full timeout.
/// - Parameters:
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectNoValue(sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectNoValue(sourceLocation: sourceLocation)
}

/// Invokes the provided assertion closure on every value emitted by the `Publisher`.
/// Useful for calling `#expect` variants where custom evaluation is required
/// - Parameters:
/// - assertion: The assertion to be performed on each emitted value of the `Publisher`
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expect(_ assertion: @escaping (Output) -> Void, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expect(assertion, sourceLocation: sourceLocation)
}

/// Invokes the provided assertion closure on every value emitted by the `Publisher`, expecting exactly `count` values emitted.
/// ⚠️ This will wait for the full timeout.
/// Useful for calling `#expect` variants where custom evaluation is required
/// - Parameters:
/// - count: The exact number of values that should be emitted from the `Publisher`
/// - assertion: The assertion to be performed on each emitted value of the `Publisher`
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectExactly(_ count: Int, _ assertion: @escaping (Output) -> Void, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectExactly(count, assertion, sourceLocation: sourceLocation)
}
}

// MARK: - Receive Completion Expectations

public extension SwiftTestingPublisherExpectation {
Expand Down Expand Up @@ -359,38 +307,6 @@ public extension SwiftTestingPublisherExpectation {
}
}

// MARK: - Publisher Extension for Receive Completion Expectations

public extension Publisher {

/// Asserts that the `Publisher` data stream completes, indifferent of the returned success/failure status (`Subscribers.Completion<Failure>`)
/// - Parameters:
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectCompletion(sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectCompletion(sourceLocation: sourceLocation)
}

/// Asserts that the `Publisher` data stream does NOT complete.
/// ⚠️ This will wait for the full timeout.
/// - Parameters:
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectNoCompletion(sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectNoCompletion(sourceLocation: sourceLocation)
}

/// Invokes the provided assertion closure on the `receiveCompletion` handler of the `Publisher`
/// Useful for calling `#expect` variants where custom evaluation is required
/// - Parameters:
/// - assertion: The assertion to be performed on the success/fail result status (`Subscribers.Completion<Failure>`)
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectCompletion(_ assertion: @escaping (Subscribers.Completion<Failure>) -> Void, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectCompletion(assertion, sourceLocation: sourceLocation)
}
}

// MARK: - Receive Success Expectations

public extension SwiftTestingPublisherExpectation {
Expand All @@ -415,19 +331,6 @@ public extension SwiftTestingPublisherExpectation {
}
}

// MARK: - Publisher Extension for Receive Success Expectations

public extension Publisher {

/// Asserts that the `Publisher` data stream completes with a success status (`.finished`)
/// - Parameters:
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `PublisherExpectation` that matches the contextual upstream `Publisher` type
func expectSuccess(sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectSuccess(sourceLocation: sourceLocation)
}
}

// MARK: - Receive Completion Failure Expectations

public extension SwiftTestingPublisherExpectation {
Expand Down Expand Up @@ -522,49 +425,6 @@ public extension SwiftTestingPublisherExpectation {
}
}

// MARK: - Publisher Extension for Receive Completion Failure Expectations

public extension Publisher {

/// Asserts that the `Publisher` data stream completes with a failure status (`.failure(Failure)`)
/// - Parameters:
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectFailure(sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectFailure(sourceLocation: sourceLocation)
}

/// Asserts that the provided `Equatable` `Failure` type is returned when the `Publisher` completes
/// - Parameters:
/// - failure: The `Equatable` `Failure` type that should be returned when the `Publisher` completes
/// - message: The message to attach to the `#expect` failure, if a mismatch is found
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectFailure(_ failure: Failure, message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> where Failure: Equatable {
.init(upstreamPublisher: self).expectFailure(failure, message: message, sourceLocation: sourceLocation)
}

/// Asserts that the `Publisher` completes with a `Failure` type which does NOT match the provided `Equatable` `Failure`
/// - Parameters:
/// - failure: The `Equatable` `Failure` type that should be returned when the `Publisher` completes
/// - message: The message to attach to the `#expect` failure, if a mismatch is found
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectNotFailure(_ failure: Failure, message: String? = nil, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> where Failure: Equatable {
.init(upstreamPublisher: self).expectNotFailure(failure, message: message, sourceLocation: sourceLocation)
}

/// Invokes the provided assertion closure on the `Failure` result's associated `Error` value of the `Publisher`
/// Useful for calling `#expect` variants where custom evaluation is required
/// - Parameters:
/// - assertion: The assertion to be performed on the `Failure` result's associated `Error` value
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectFailure(_ assertion: @escaping (Failure) -> Void, sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectFailure(assertion, sourceLocation: sourceLocation)
}
}

// MARK: - Void Publisher Expectations

public extension SwiftTestingPublisherExpectation where UpstreamPublisher.Output == Void {
Expand All @@ -588,19 +448,6 @@ public extension SwiftTestingPublisherExpectation where UpstreamPublisher.Output
}
}

// MARK: - Publisher Extension for Void Publisher Expectations

public extension Publisher where Output == Void {

/// Asserts that `Void` will be emitted by the `Publisher` one or more times
/// - Parameters:
/// - sourceLocation: The calling source. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `SwiftTestingPublisherExpectation` that matches the contextual upstream `Publisher` type
func expectVoid(sourceLocation: SourceLocation = #_sourceLocation) -> SwiftTestingPublisherExpectation<Self> {
.init(upstreamPublisher: self).expectVoid(sourceLocation: sourceLocation)
}
}

// MARK: - Private

private extension SwiftTestingPublisherExpectation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import Combine
import Testing

struct ExampleTest {

@Test func fail() async {
await withKnownIssue() {
await withKnownIssue {
let publisher = CurrentValueSubject<String, Error>("foo")
await publisher
.testable()
.expect("bar")
.expectSuccess()
.waitForExpectations(timeout: 1)
Expand All @@ -23,6 +24,7 @@ struct ExampleTest {
@Test func pass() async {
let publisher = ["baz"].publisher
await publisher
.testable()
.expect("baz")
.expectSuccess()
.waitForExpectations(timeout: 1)
Expand Down
Loading

0 comments on commit 9ac94c5

Please sign in to comment.