From 7a513663b6f68c19a1bc124ac4f121b1d68682a9 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Wed, 1 Jan 2025 11:16:19 +1300 Subject: [PATCH] Make waiting for multiple expectations easy --- README.md | 19 +++ Sources/TestingExpectation/Expectations.swift | 62 ++++++++++ .../ExpectationsTests.swift | 116 ++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 Sources/TestingExpectation/Expectations.swift create mode 100644 Tests/TestingExpectationTests/ExpectationsTests.swift diff --git a/README.md b/README.md index 8300bef..ff06e6c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,25 @@ The `Expectation` vended from this library fills that gap: } ``` +### Waiting for multiple expectations + +The `Expectations` type vended from this library makes it easy to wait for multiple expectations: + +```swift +@Test func testMethodEventuallyTriggersClosures() async { + let expectation1 = Expectation() + let expectation2 = Expectation() + let expectation3 = Expectation() + + systemUnderTest.closure1 = { expectation1.fulfill() } + systemUnderTest.closure2 = { expectation2.fulfill() } + systemUnderTest.closure3 = { expectation3.fulfill() } + systemUnderTest.method() + + await Expectations(expectation1, expectation2, expectation3).fulfillment(within: .seconds(5)) +} +``` + ## Installation ### Swift Package Manager diff --git a/Sources/TestingExpectation/Expectations.swift b/Sources/TestingExpectation/Expectations.swift new file mode 100644 index 0000000..35f6ee8 --- /dev/null +++ b/Sources/TestingExpectation/Expectations.swift @@ -0,0 +1,62 @@ +// MIT License +// +// Copyright (c) 2025 Dan Federman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +public actor Expectations { + // MARK: Initialization + + public init(_ expectations: [Expectation]) { + self.expectations = expectations + } + + public init(_ expectations: Expectation...) { + self.init(expectations) + } + + // MARK: Public + + public func fulfillment( + within duration: Duration, + filePath: String = #filePath, + fileID: String = #fileID, + line: Int = #line, + column: Int = #column + ) async { + await withTaskGroup(of: Void.self) { taskGroup in + for expectation in expectations { + taskGroup.addTask { + await expectation.fulfillment( + within: duration, + filePath: filePath, + fileID: fileID, + line: line, + column: column + ) + } + } + await taskGroup.waitForAll() + } + } + + // MARK: Private + + private let expectations: [Expectation] +} diff --git a/Tests/TestingExpectationTests/ExpectationsTests.swift b/Tests/TestingExpectationTests/ExpectationsTests.swift new file mode 100644 index 0000000..8473153 --- /dev/null +++ b/Tests/TestingExpectationTests/ExpectationsTests.swift @@ -0,0 +1,116 @@ +// MIT License +// +// Copyright (c) 2025 Dan Federman +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Testing + +@testable import TestingExpectation + +struct ExpectationsTests { + @Test + func test_fulfillment_doesNotWaitIfAlreadyFulfilled() async { + let expectation = Expectation(expectedCount: 0) + await Expectations(expectation).fulfillment(within: .seconds(10)) + } + + @MainActor // Global actor ensures Task ordering. + @Test + func test_fulfillment_waitsForFulfillmentOfSingleExpectation() async { + let expectation = Expectation(expectedCount: 1) + var hasFulfilled = false + let wait = Task { + await Expectations(expectation).fulfillment(within: .seconds(10)) + #expect(hasFulfilled) + } + Task { + expectation.fulfill() + hasFulfilled = true + } + await wait.value + } + + @MainActor // Global actor ensures Task ordering. + @Test + func test_fulfillment_waitsForFulfillmentOfMultipleExpectations() async { + let expectation1 = Expectation(expectedCount: 1) + let expectation2 = Expectation(expectedCount: 1) + let expectation3 = Expectation(expectedCount: 1) + var hasFulfilled = false + let wait = Task { + await Expectations(expectation1).fulfillment(within: .seconds(10)) + #expect(hasFulfilled) + } + Task { + expectation1.fulfill() + expectation2.fulfill() + expectation3.fulfill() + hasFulfilled = true + } + await wait.value + } + + @Test + func test_fulfillment_triggersFalseExpectationWhenSingleExpectationTimesOut() async { + await confirmation { confirmation in + let expectation = Expectation( + expectedCount: 1, + expect: { expectation, _, _ in + #expect(!expectation) + confirmation() + } + ) + let systemUnderTest = Expectations(expectation) + await systemUnderTest.fulfillment(within: .zero) + } + } + + @Test + func test_fulfillment_triggersFalseExpectationWhenSingleExpectationOfManyTimesOut() async { + await confirmation(expectedCount: 3) { confirmation in + let expectation1 = Expectation( + expectedCount: 1, + expect: { expectation, _, _ in + #expect(!expectation) + confirmation() + } + ) + let expectation2 = Expectation( + expectedCount: 1, + expect: { expectation, _, _ in + #expect(expectation) + confirmation() + } + ) + expectation2.fulfill() + let expectation3 = Expectation( + expectedCount: 1, + expect: { expectation, _, _ in + #expect(expectation) + confirmation() + } + ) + expectation3.fulfill() + + let systemUnderTest = Expectations(expectation1, expectation2, expectation3) + await systemUnderTest.fulfillment(within: .zero) + } + } +}