Skip to content

Commit 65d5322

Browse files
committed
[fix]: improve optional handling in TestApplyContext
1 parent ff4aaea commit 65d5322

File tree

2 files changed

+79
-9
lines changed

2 files changed

+79
-9
lines changed

WorkflowTesting/Sources/WorkflowActionTester.swift

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import XCTest
2121
@testable import Workflow
2222

2323
extension WorkflowAction {
24-
/// Returns a state tester containing `self`.
24+
/// Returns a `WorkflowActionTester` with the given state before the `WorkflowAction` has been applied to it.
25+
///
26+
/// - Parameters:
27+
/// - state: The `WorkflowType.State` instance that specifies the state before the `WorkflowAction` has been applied.
28+
/// - workflow: An optional `WorkflowType` instance to be used if the `WorkflowAction` needs to read workflow properties off of the `ApplyContext` parameter during action application. If this parameter is unspecified, attempts to access the `WorkflowType`'s properties will error in the testing runtime.
29+
/// - Returns: An appropriately-configured `WorkflowActionTester`.
2530
public static func tester(
2631
withState state: WorkflowType.State,
2732
workflow: WorkflowType? = nil
@@ -70,6 +75,19 @@ extension WorkflowAction {
7075
/// .assert(output: .finished)
7176
/// .assert(state: .differentState)
7277
/// ```
78+
///
79+
/// If the `Action` under test uses the runtime's `ApplyContext` to read values from the
80+
/// current `Workflow` instance, then an instance of the `Workflow` with the expected
81+
/// properties that will be read during `send(action:)` must be supplied like:
82+
/// ```
83+
/// MyWorkflow.Action
84+
/// .tester(
85+
/// withState: .firstState,
86+
/// workflow: MyWorkflow(prop: 42)
87+
/// )
88+
/// .send(action: .exampleActionThatReadsWorkflowProp)
89+
/// .assert(...)
90+
/// ```
7391
public struct WorkflowActionTester<WorkflowType, Action: WorkflowAction> where Action.WorkflowType == WorkflowType {
7492
/// The current state
7593
let state: WorkflowType.State
@@ -209,10 +227,25 @@ struct TestApplyContext<Wrapped: Workflow>: ApplyContextType {
209227
case .workflow(let workflow):
210228
return workflow[keyPath: keyPath]
211229
case .expectations(var expectedValues):
212-
guard let value = expectedValues.removeValue(forKey: keyPath) as? Value else {
213-
fatalError("Attempted to read value \(keyPath as AnyKeyPath), when applying an action, but no value was present. Pass an instance of the Workflow to the ActionTester to enable this functionality.")
230+
guard
231+
// We have an expected value
232+
let value = expectedValues.removeValue(forKey: keyPath),
233+
// And it's the right type
234+
let value = value as? Value
235+
else {
236+
// We're expecting a value of optional type. Error, but don't crash
237+
// since we can just return nil.
238+
if Value.self is OptionalProtocol.Type {
239+
reportIssue("Attempted to read value \(keyPath as AnyKeyPath), when applying an action, but no value was present. Pass an instance of the Workflow to the ActionTester to enable this functionality.")
240+
return Any?.none as! Value
241+
} else {
242+
fatalError("Attempted to read value \(keyPath as AnyKeyPath), when applying an action, but no value was present. Pass an instance of the Workflow to the ActionTester to enable this functionality.")
243+
}
214244
}
215245
return value
216246
}
217247
}
218248
}
249+
250+
private protocol OptionalProtocol {}
251+
extension Optional: OptionalProtocol {}

WorkflowTesting/Tests/WorkflowActionTesterTests.swift

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17+
import IssueReporting
1718
import Workflow
1819
import XCTest
20+
1921
@testable import WorkflowTesting
2022

2123
final class WorkflowActionTesterTests: XCTestCase {
@@ -112,14 +114,34 @@ extension WorkflowActionTesterTests {
112114
withState: true,
113115
workflow: TestWorkflow(prop: 42)
114116
)
115-
.send(action: .readProps)
117+
.send(action: .readProp)
118+
.assert(state: true)
119+
.assert(output: .value("read prop: 42"))
120+
}
121+
122+
func test_new_api_works_with_optional_props() {
123+
TestActionWithProps
124+
.tester(
125+
withState: true,
126+
workflow: TestWorkflow(prop: 42, optionalProp: 22)
127+
)
128+
.send(action: .readOptionalProp)
129+
.assert(state: true)
130+
.assert(output: .value("read optional prop: 22"))
131+
132+
TestActionWithProps
133+
.tester(
134+
withState: true,
135+
workflow: TestWorkflow(prop: 42, optionalProp: nil)
136+
)
137+
.send(action: .readOptionalProp)
116138
.assert(state: true)
117-
.assert(output: .value("read props: 42"))
139+
.assert(output: .value("read optional prop: <nil>"))
118140
}
119141

120142
// FIXME: ideally an 'exit/death test' would somehow be used for this...
121143
/*
122-
func test_old_api_explodes_if_you_use_props() {
144+
func test_old_api_explodes_if_accessing_through_apply_context() {
123145
XCTExpectFailure("This test should fail")
124146

125147
TestActionWithProps
@@ -128,14 +150,24 @@ extension WorkflowActionTesterTests {
128150
.assert(state: true)
129151
}
130152
*/
153+
154+
func test_old_api_errors_accessing_optional_through_apply_context_without_proper_setup() {
155+
withExpectedIssue("reading optional value through context without workflow should fail but not crash") {
156+
TestActionWithProps
157+
.tester(withState: true)
158+
.send(action: .readOptionalProp)
159+
.assert(state: true)
160+
}
161+
}
131162
}
132163

133164
// MARK: -
134165

135166
private enum TestActionWithProps: WorkflowAction {
136167
typealias WorkflowType = TestWorkflow
137168

138-
case readProps
169+
case readProp
170+
case readOptionalProp
139171
case dontReadProps
140172

141173
func apply(
@@ -146,9 +178,13 @@ private enum TestActionWithProps: WorkflowAction {
146178
case .dontReadProps:
147179
return .value("did not read props")
148180

149-
case .readProps:
181+
case .readProp:
150182
let prop = context[workflowValue: \.prop]
151-
return .value("read props: \(prop)")
183+
return .value("read prop: \(prop)")
184+
185+
case .readOptionalProp:
186+
let optionalProp = context[workflowValue: \.optionalProp]
187+
return .value("read optional prop: \(optionalProp?.description ?? "<nil>")")
152188
}
153189
}
154190
}
@@ -179,6 +215,7 @@ private struct TestWorkflow: Workflow {
179215
}
180216

181217
var prop = 0
218+
var optionalProp: Int? = 42
182219

183220
func makeInitialState() -> Bool {
184221
true

0 commit comments

Comments
 (0)