Skip to content

Commit 07f4257

Browse files
authored
[fix]: improve optional handling in TestApplyContext (#361)
this change makes two improvements to the current implementation of `TestApplyContext`: 1. it removes the partially-implemented 'expectations dictionary' functionality from the `TestApplyContext` in favor of an alternate case denoting the fact that client code provided no mechanism to resolve `Workflow` values at runtime 2. it allows failures in such a mode to resolve optional workflow property values from the `TestApplyContext` so tests can continue running rather than resulting in a fatal error
1 parent ff4aaea commit 07f4257

File tree

2 files changed

+80
-12
lines changed

2 files changed

+80
-12
lines changed

WorkflowTesting/Sources/WorkflowActionTester.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ 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
2833
) -> WorkflowActionTester<WorkflowType, Self> {
2934
WorkflowActionTester(
3035
state: state,
3136
context: TestApplyContext(
32-
kind: workflow.map { .workflow($0) } ?? .expectations([:])
37+
kind: workflow.map { .workflow($0) } ?? .unsupported
3338
)
3439
)
3540
}
@@ -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
@@ -193,7 +211,10 @@ struct TestApplyContext<Wrapped: Workflow>: ApplyContextType {
193211
// FIXME: flesh this out to support 'just in time' values
194212
// rather than requiring a full Workflow instance to be provided
195213
// https://github.com/square/workflow-swift/issues/351
196-
case expectations([AnyKeyPath: Any])
214+
// case expectations([AnyKeyPath: Any])
215+
216+
/// The client provided no means to resolve values at runtime, and lookups will fail with a runtime error
217+
case unsupported
197218
}
198219

199220
var storage: TestContextKind
@@ -208,11 +229,21 @@ struct TestApplyContext<Wrapped: Workflow>: ApplyContextType {
208229
switch storage {
209230
case .workflow(let workflow):
210231
return workflow[keyPath: keyPath]
211-
case .expectations(var expectedValues):
212-
guard let value = expectedValues.removeValue(forKey: keyPath) as? Value else {
232+
// TODO: implement https://github.com/square/workflow-swift/issues/351
233+
// case .expectations:
234+
// fatalError("Not yet supported")
235+
case .unsupported:
236+
// If 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 {
213242
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.")
214243
}
215-
return value
216244
}
217245
}
218246
}
247+
248+
private protocol OptionalProtocol {}
249+
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)