Skip to content

Commit

Permalink
Added expectExactly function
Browse files Browse the repository at this point in the history
  • Loading branch information
Albert committed Jun 16, 2022
1 parent c4ef017 commit 74c8408
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 19 deletions.
86 changes: 84 additions & 2 deletions Sources/TestableCombinePublishers/PublisherExpectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public final class PublisherExpectation<UpstreamPublisher: Publisher> {

public extension PublisherExpectation {

/// Asserts that the provided `Equatable` value will be emitted by the `Publisher`
/// Asserts that the provided `Equatable` value will be emitted by the `Publisher` one or more times
/// - Parameters:
/// - expected: The `Equatable` value expected from the `Publisher`
/// - message: The message to attach to the `XCTAssertEqual` failure, if a mismatch is found
Expand All @@ -104,6 +104,34 @@ public extension PublisherExpectation {
return self
}

/// Asserts that the provided `Equatable` value will be emitted by the `Publisher` exactly `count` times.
/// ⚠️ This will wait for the full timeout in `waitForExpectations(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 `XCTAssertEqual` failure, if a mismatch is found
/// - file: The calling file. Used for showing context-appropriate unit test failures in Xcode
/// - line: The calling line of code. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `PublisherExpectation` that matches the contextual upstream `Publisher` type
func expectExactly(_ count: Int, of expected: UpstreamPublisher.Output, message: String? = nil, file: StaticString = #filePath, line: UInt = #line) -> Self where UpstreamPublisher.Output: Equatable {
let minExpectation = LocatableTestExpectation(description: "min expectExactly(\(count), of: \(expected))", file: file, line: line)
minExpectation.expectedFulfillmentCount = count
let maxExpectation = LocatableTestExpectation(description: "max expectExactly(\(count), of: \(expected))", file: file, line: line)
maxExpectation.isInverted = true
maxExpectation.expectedFulfillmentCount = count + 1
expectations.append(minExpectation)
expectations.append(maxExpectation)
upstreamPublisher
.sink { completion in
} receiveValue: { value in
XCTAssertEqual(expected, value, message ?? "", file: file, line: line)
minExpectation.fulfill()
maxExpectation.fulfill()
}
.store(in: &cancellables)
return self
}

/// 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`
Expand Down Expand Up @@ -143,7 +171,7 @@ public extension PublisherExpectation {
return self
}

/// Invokes the provided assertion closure on every value emitted by the `Publisher`.
/// Invokes the provided assertion closure on every value emitted by the `Publisher`, expecting at least one `Output` value.
/// Useful for calling `XCTAssert` variants where custom evaluation is required
/// - Parameters:
/// - assertion: The assertion to be performed on each emitted value of the `Publisher`
Expand All @@ -162,6 +190,34 @@ public extension PublisherExpectation {
.store(in: &cancellables)
return self
}

/// Invokes the provided assertion closure on every value emitted by the `Publisher`, expecting exactly `count` values emitted.
/// ⚠️ This will wait for the full timeout in `waitForExpectations(timeout:)`
/// Useful for calling `XCTAssert` 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`
/// - file: The calling file. Used for showing context-appropriate unit test failures in Xcode
/// - line: The calling line of code. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `PublisherExpectation` that matches the contextual upstream `Publisher` type
func expectExactly(_ count: Int, _ assertion: @escaping (UpstreamPublisher.Output) -> Void, file: StaticString = #filePath, line: UInt = #line) -> Self {
let minExpectation = LocatableTestExpectation(description: "min expectExactly(\(count) assertion)", file: file, line: line)
minExpectation.expectedFulfillmentCount = count
let maxExpectation = LocatableTestExpectation(description: "max expectExactly(\(count) assertion)", file: file, line: line)
maxExpectation.isInverted = true
maxExpectation.expectedFulfillmentCount = count + 1
expectations.append(minExpectation)
expectations.append(maxExpectation)
upstreamPublisher
.sink { completion in
} receiveValue: { value in
assertion(value)
minExpectation.fulfill()
maxExpectation.fulfill()
}
.store(in: &cancellables)
return self
}
}

public extension Publisher {
Expand All @@ -176,6 +232,19 @@ public extension Publisher {
func expect(_ expected: Output, message: String? = nil, file: StaticString = #filePath, line: UInt = #line) -> PublisherExpectation<Self> where Output: Equatable {
.init(upstream: self).expect(expected, message: message, file: file, line: line)
}

/// Asserts that the provided `Equatable` value will be emitted by the `Publisher` exactly `count` times.
/// ⚠️ This will wait for the full timeout in `waitForExpectations(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 `XCTAssertEqual` failure, if a mismatch is found
/// - file: The calling file. Used for showing context-appropriate unit test failures in Xcode
/// - line: The calling line of code. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `PublisherExpectation` that matches the contextual upstream `Publisher` type
func expectExactly(_ count: Int, of expected: Output, message: String? = nil, file: StaticString = #filePath, line: UInt = #line) -> PublisherExpectation<Self> where Output: Equatable {
.init(upstream: self).expectExactly(count, of: expected, message: message, file: file, line: line)
}

/// Asserts that a value will be emitted by the `Publisher` and that it does NOT match the provided `Equatable`
/// - Parameters:
Expand Down Expand Up @@ -208,6 +277,19 @@ public extension Publisher {
func expect(_ assertion: @escaping (Output) -> Void, file: StaticString = #filePath, line: UInt = #line) -> PublisherExpectation<Self> {
.init(upstream: self).expect(assertion, file: file, line: line)
}

/// Invokes the provided assertion closure on every value emitted by the `Publisher`, expecting exactly `count` values emitted.
/// ⚠️ This will wait for the full timeout in `waitForExpectations(timeout:)`
/// Useful for calling `XCTAssert` 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`
/// - file: The calling file. Used for showing context-appropriate unit test failures in Xcode
/// - line: The calling line of code. Used for showing context-appropriate unit test failures in Xcode
/// - Returns: A chainable `PublisherExpectation` that matches the contextual upstream `Publisher` type
func expectExactly(_ count: Int, _ assertion: @escaping (Output) -> Void, file: StaticString = #filePath, line: UInt = #line) -> PublisherExpectation<Self> {
.init(upstream: self).expectExactly(count, assertion, file: file, line: line)
}
}

// MARK: - Receive Completion Expectations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ final class TestableCombinePublishersTests: XCTestCase {

// MARK: - Receive Value Expectations

func testSingleEquatableValue() {
func testExpectEquatableValue() {
["cool"]
.publisher
.expect("cool")
.waitForExpectations(timeout: 1)

["cool", "cool", "cool"]
.publisher
.expect("cool")
.waitForExpectations(timeout: 1)
}

func testSingleEquatableValueFail() {
func testExpectEquatableValueFail() {
XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
Expand All @@ -52,14 +57,47 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testExpectNotValue() {
func testExpectExactlyCountOfEquatableValues() {
["cool", "cool"]
.publisher
.expectExactly(2, of: "cool")
.waitForExpectations(timeout: 1)
}

func testExpectExactlyCountOfEquatableValuesFail() {
XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
.expectExactly(2, of: "neat")
.waitForExpectations(timeout: 1)

XCTExpectFailure("Incorrect assertion should fail")
[String]()
.publisher
.expectExactly(2, of: "neat")
.waitForExpectations(timeout: 1)

XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
.expectExactly(2, of: "cool")
.waitForExpectations(timeout: 1)

XCTExpectFailure("Incorrect assertion should fail")
["cool", "cool", "cool"]
.publisher
.expectExactly(2, of: "cool")
.waitForExpectations(timeout: 1)
}

func testExpectNotEquatableValue() {
["neat"]
.publisher
.expectNot("cool")
.waitForExpectations(timeout: 1)
}

func testExpectNotFail() {
func testExpectNotEquatableValueFail() {
XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
Expand Down Expand Up @@ -88,15 +126,15 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testSingleValueClosure() async {
func testExpectValueClosure() async {
["cool"]
.publisher
.expect({ XCTAssertEqual("cool", $0) })
.waitForExpectations(timeout: 1)

}

func testSingleValueClosureFailure() async {
func testExpectValueClosureFail() async {
XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
Expand All @@ -111,6 +149,41 @@ final class TestableCombinePublishersTests: XCTestCase {

}

func testExpectExactlyCountOfValueClosure() async {
["cool", "cool"]
.publisher
.expectExactly(2, { XCTAssertEqual("cool", $0) })
.waitForExpectations(timeout: 1)

}

func testExpectExactlyCountOfValueClosureFail() async {
XCTExpectFailure("Incorrect assertion should fail")
["cool", "cool"]
.publisher
.expectExactly(2, { XCTAssertEqual("neat", $0) })
.waitForExpectations(timeout: 1)

XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
.expectExactly(2, { XCTAssertEqual("cool", $0) })
.waitForExpectations(timeout: 1)

XCTExpectFailure("Incorrect assertion should fail")
["cool", "cool", "cool"]
.publisher
.expectExactly(2, { XCTAssertEqual("cool", $0) })
.waitForExpectations(timeout: 1)

XCTExpectFailure("Incorrect assertion should fail")
["cool", "cool", "neat"]
.publisher
.expectExactly(2, { XCTAssertEqual("cool", $0) })
.waitForExpectations(timeout: 1)

}

func testMulitpleEquatableValues() async {
["cool", "neat", "awesome"]
.publisher
Expand All @@ -119,7 +192,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testMulitpleEquatableValuesFailure() async {
func testMulitpleEquatableValuesFail() async {
XCTExpectFailure("Incorrect assertion should fail")
["cool", "neat", "awesome"]
.publisher
Expand All @@ -143,7 +216,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testMulitpleValuesClosureFailure() async {
func testMulitpleValuesClosureFail() async {
XCTExpectFailure("Incorrect assertion should fail")
["cool", "neat", "awesome"]
.publisher
Expand All @@ -161,7 +234,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testCompletionFailure() {
func testCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
PassthroughSubject<Void, Never>()
.expectCompletion()
Expand All @@ -174,7 +247,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testNoCompletionFailure() {
func testNoCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
[Int]()
.publisher
Expand All @@ -189,7 +262,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testCompletionClosureFailure() {
func testCompletionClosureFail() {
XCTExpectFailure("Incorrect assertion should fail")
Fail<Void, MockError>(error: MockError.someProblem)
.expectCompletion({ XCTAssertEqual($0, .finished) })
Expand All @@ -210,7 +283,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testSuccessCompletionFailure() {
func testSuccessCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
PassthroughSubject<Void, Never>()
.expectSuccess()
Expand All @@ -225,7 +298,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testFailureCompletionFailure() {
func testFailureCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
PassthroughSubject<Void, MockError>()
.expectFailure()
Expand All @@ -238,7 +311,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testFailureValueCompletionFailure() {
func testFailureValueCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
PassthroughSubject<Void, MockError>()
.expectFailure(MockError.someProblem)
Expand All @@ -256,7 +329,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testNotFailureValueCompletionFailure() {
func testNotFailureValueCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
PassthroughSubject<Void, MockError>()
.expectNotFailure(MockError.someProblem)
Expand All @@ -274,7 +347,7 @@ final class TestableCombinePublishersTests: XCTestCase {
.waitForExpectations(timeout: 1)
}

func testFailureClosureCompletionFailure() {
func testFailureClosureCompletionFail() {
XCTExpectFailure("Incorrect assertion should fail")
Fail<Void, MockError>(error: MockError.someProblem)
.expectFailure({ XCTAssertEqual($0, .otherProblem) })
Expand Down Expand Up @@ -315,7 +388,7 @@ final class TestableCombinePublishersTests: XCTestCase {
}

// TODO: This is currently failing. The expectations should respect sequence, if possible.
func testOrderFailure() {
func testOrderFail() {
XCTExpectFailure("Incorrect assertion should fail")
["cool"]
.publisher
Expand Down

0 comments on commit 74c8408

Please sign in to comment.