Skip to content

[chore]: improve Swift Testing support in test utilities #341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions WorkflowCombine/Testing/WorkerTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#if DEBUG
import IssueReporting
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for my education: where is this lib coming from? Apple side?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a pointfree library – it's a dependency of the CustomDump lib that we already use: https://github.com/pointfreeco/swift-issue-reporting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though now that you bring it up... i wonder if it should be listed as an explicit dependency in the package?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import Workflow
import WorkflowTesting
import XCTest
Expand Down Expand Up @@ -62,9 +63,9 @@ extension RenderTester {
guard !workflow.worker.isEquivalent(to: expectedWorker) else {
return
}
XCTFail(
reportIssue(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no Apple-provided way to do this?

Copy link
Contributor Author

@jamieQ jamieQ May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by 'this', what do you mean? record an issue in either testing framework? if so, i don't think so, at least not that i could easily find.

"Workers of type \(ExpectedWorkerType.self) not equivalent. Expected: \(expectedWorker). Got: \(workflow.worker)",
file: file,
filePath: file,
line: line
)
}
Expand Down
5 changes: 3 additions & 2 deletions WorkflowConcurrency/Testing/WorkerTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#if DEBUG
import IssueReporting
import Workflow
import WorkflowTesting
import XCTest
Expand Down Expand Up @@ -62,9 +63,9 @@ extension RenderTester {
guard !workflow.worker.isEquivalent(to: worker) else {
return
}
XCTFail(
reportIssue(
"Workers of type \(ExpectedWorkerType.self) not equivalent. Expected: \(worker). Got: \(workflow.worker)",
file: file,
filePath: file,
line: line
)
}
Expand Down
5 changes: 3 additions & 2 deletions WorkflowReactiveSwift/Testing/WorkerTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#if DEBUG
import IssueReporting
import Workflow
import WorkflowTesting
import XCTest
Expand Down Expand Up @@ -62,9 +63,9 @@ extension RenderTester {
guard !workflow.worker.isEquivalent(to: expectedWorker) else {
return
}
XCTFail(
reportIssue(
"Workers of type \(ExpectedWorkerType.self) not equivalent. Expected: \(expectedWorker). Got: \(workflow.worker)",
file: file,
filePath: file,
line: line
)
}
Expand Down
5 changes: 3 additions & 2 deletions WorkflowRxSwift/Testing/WorkerTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

#if DEBUG
import IssueReporting
import Workflow
import WorkflowTesting
import XCTest
Expand Down Expand Up @@ -62,9 +63,9 @@ extension RenderTester {
guard !workflow.worker.isEquivalent(to: expectedWorker) else {
return
}
XCTFail(
reportIssue(
"Workers of type \(ExpectedWorkerType.self) not equivalent. Expected: \(expectedWorker). Got: \(workflow.worker)",
file: file,
filePath: file,
line: line
)
}
Expand Down
3 changes: 2 additions & 1 deletion WorkflowTesting/Sources/Internal/AppliedAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import IssueReporting
import Workflow
import XCTest

Expand All @@ -26,7 +27,7 @@ struct AppliedAction<WorkflowType: Workflow> {

func assert<ActionType: WorkflowAction>(type: ActionType.Type = ActionType.self, file: StaticString, line: UInt, assertions: (ActionType) throws -> Void) rethrows where ActionType.WorkflowType == WorkflowType {
guard let action = erasedAction as? ActionType else {
XCTFail("Expected action of type \(ActionType.self), got \(erasedAction)", file: file, line: line)
reportIssue("Expected action of type \(ActionType.self), got \(erasedAction)", filePath: file, line: line)
return
}
try assertions(action)
Expand Down
20 changes: 13 additions & 7 deletions WorkflowTesting/Sources/Internal/RenderTester+TestContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

#if DEBUG

import IssueReporting
import XCTest

@testable import Workflow

extension RenderTester {
Expand Down Expand Up @@ -65,7 +67,7 @@ extension RenderTester {
"""
}
let failureMessage = "Attempted to render unexpected Workflow of type \(Child.self) with key \"\(key)\". \(diagnosticMessage)"
XCTFail(failureMessage, file: file, line: line)
reportIssue(failureMessage, filePath: file, line: line)

// We can “recover” from missing Void-rendering workflows since there’s only one possible value to return
if Child.Rendering.self == Void.self {
Expand All @@ -76,7 +78,7 @@ extension RenderTester {
}
let (inserted, _) = usedWorkflowKeys.insert(WorkflowKey(type: ObjectIdentifier(Child.self), key: key))
if !inserted {
XCTFail("Multiple Workflows of type \(Child.self) with key \"\(key)\" used in the same render call. Use a unique key to render multiple Workflows of the same type.", file: file, line: line)
reportIssue("Multiple Workflows of type \(Child.self) with key \"\(key)\" used in the same render call. Use a unique key to render multiple Workflows of the same type.", filePath: file, line: line)
}

expectedWorkflows.removeAll(where: { $0 === expectedWorkflow })
Expand All @@ -96,7 +98,7 @@ extension RenderTester {

func runSideEffect(key: AnyHashable, action: (Lifetime) -> Void) {
guard let sideEffect = expectedSideEffects.removeValue(forKey: key) else {
XCTFail("Unexpected side-effect with key \"\(key)\"", file: file, line: line)
reportIssue("Unexpected side-effect with key \"\(key)\"", filePath: file, line: line)
return
}

Expand All @@ -106,21 +108,25 @@ extension RenderTester {
/// Validate the expectations were fulfilled, or fail if not.
func assertNoLeftOverExpectations() {
for expectedWorkflow in expectedWorkflows {
XCTFail("Expected child workflow of type: \(expectedWorkflow.workflowType), key: \"\(expectedWorkflow.key)\"", file: file, line: expectedWorkflow.line)
reportIssue("Expected child workflow of type: \(expectedWorkflow.workflowType), key: \"\(expectedWorkflow.key)\"", filePath: file, line: expectedWorkflow.line)
}

for (key, expectedSideEffect) in expectedSideEffects {
XCTFail("Expected side-effect with key: \"\(key)\"", file: expectedSideEffect.file, line: expectedSideEffect.line)
reportIssue("Expected side-effect with key: \"\(key)\"", filePath: expectedSideEffect.file, line: expectedSideEffect.line)
}
}

private func apply<ActionType: WorkflowAction>(action: ActionType) where ActionType.WorkflowType == WorkflowType {
XCTAssertNil(appliedAction, "Received multiple actions in a single render test", file: file, line: line)
if appliedAction != nil {
reportIssue("Received multiple actions in a single render test", filePath: file, line: line)
}
appliedAction = AppliedAction(action)
let output = action.apply(toState: &state)

if let output {
XCTAssertNil(producedOutput, "Received multiple outputs in a single render test", file: file, line: line)
if producedOutput != nil {
reportIssue("Received multiple outputs in a single render test", filePath: file, line: line)
}
producedOutput = output
}
}
Expand Down
9 changes: 5 additions & 4 deletions WorkflowTesting/Sources/RenderTesterResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import CustomDump
import IssueReporting
import Workflow
import XCTest

Expand Down Expand Up @@ -56,7 +57,7 @@ public struct RenderTesterResult<WorkflowType: Workflow> {
line: UInt = #line
) -> RenderTesterResult<WorkflowType> {
if let appliedAction {
XCTFail("Expected no action, but got \(appliedAction.erasedAction).", file: file, line: line)
reportIssue("Expected no action, but got \(appliedAction.erasedAction).", filePath: file, line: line)
}
return self
}
Expand All @@ -70,7 +71,7 @@ public struct RenderTesterResult<WorkflowType: Workflow> {
assertions: (ActionType) throws -> Void
) rethrows -> RenderTesterResult<WorkflowType> where ActionType.WorkflowType == WorkflowType {
guard let appliedAction else {
XCTFail("No action was produced", file: file, line: line)
reportIssue("No action was produced", filePath: file, line: line)
return self
}
try appliedAction.assert(file: file, line: line, assertions: assertions)
Expand Down Expand Up @@ -106,7 +107,7 @@ public struct RenderTesterResult<WorkflowType: Workflow> {
line: UInt = #line
) -> RenderTesterResult<WorkflowType> {
if let output {
XCTFail("Expected no output, but got \(output).", file: file, line: line)
reportIssue("Expected no output, but got \(output).", filePath: file, line: line)
}
return self
}
Expand All @@ -119,7 +120,7 @@ public struct RenderTesterResult<WorkflowType: Workflow> {
assertions: (WorkflowType.Output) throws -> Void
) rethrows -> RenderTesterResult<WorkflowType> {
guard let output else {
XCTFail("No output was produced", file: file, line: line)
reportIssue("No output was produced", filePath: file, line: line)
return self
}
try assertions(output)
Expand Down
10 changes: 6 additions & 4 deletions WorkflowTesting/Sources/WorkflowActionTester.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import CustomDump
import IssueReporting
import Workflow
import XCTest

Expand Down Expand Up @@ -91,7 +93,7 @@ public struct WorkflowActionTester<WorkflowType, Action: WorkflowAction> where A
line: UInt = #line
) -> WorkflowActionTester<WorkflowType, Action> {
if let output {
XCTFail("Expected no output, but got \(output).", file: file, line: line)
reportIssue("Expected no output, but got \(output).", filePath: file, line: line)
}
return self
}
Expand All @@ -109,7 +111,7 @@ public struct WorkflowActionTester<WorkflowType, Action: WorkflowAction> where A
_ assertions: (WorkflowType.Output) throws -> Void
) rethrows -> WorkflowActionTester<WorkflowType, Action> {
guard let output else {
XCTFail("No output was produced", file: file, line: line)
reportIssue("No output was produced", filePath: file, line: line)
return self
}
try assertions(output)
Expand Down Expand Up @@ -143,7 +145,7 @@ extension WorkflowActionTester where WorkflowType.State: Equatable {
@discardableResult
public func assert(state expectedState: WorkflowType.State, file: StaticString = #file, line: UInt = #line) -> WorkflowActionTester<WorkflowType, Action> {
verifyState { actualState in
XCTAssertEqual(actualState, expectedState, file: file, line: line)
expectNoDifference(actualState, expectedState, filePath: file, line: line)
}
}
}
Expand All @@ -157,7 +159,7 @@ extension WorkflowActionTester where WorkflowType.Output: Equatable {
@discardableResult
public func assert(output expectedOutput: WorkflowType.Output, file: StaticString = #file, line: UInt = #line) -> WorkflowActionTester<WorkflowType, Action> {
verifyOutput { actualOutput in
XCTAssertEqual(actualOutput, expectedOutput, file: file, line: line)
expectNoDifference(actualOutput, expectedOutput, filePath: file, line: line)
}
}
}
65 changes: 65 additions & 0 deletions WorkflowTesting/Tests/TestingFrameworkCompatibilityTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Testing
import Workflow
import XCTest

@testable import WorkflowTesting

struct SwiftTestingCompatibilityTests {
@Test
func testInternalFailureRecordsExpectationFailure_swiftTesting() {
withKnownIssue {
TestAction
.tester(withState: false)
.send(action: .change(true))
.assertNoOutput() // should fail the test
}
}
}

final class XCTestCompatibilityTests: XCTestCase {
func testInternalFailureRecordsExpectationFailure_xctest() {
XCTExpectFailure {
_ = TestAction
.tester(withState: false)
.send(action: .change(true))
.assertNoOutput() // should fail the test
}
}
}

private enum TestAction: WorkflowAction {
typealias WorkflowType = TestWorkflow

case change(Bool)

func apply(toState state: inout Bool) -> TestWorkflow.Output? {
if case .change(let newState) = self {
state = newState
}
return 42
}
}

private struct TestWorkflow: Workflow {
typealias Rendering = Void
typealias Output = Int

func makeInitialState() -> Bool { true }
func render(state: Bool, context: RenderContext<TestWorkflow>) {}
}
Loading