Testing delegate actions with case key paths #2646
Replies: 5 comments 7 replies
-
We actually did consider this style of |
Beta Was this translation helpful? Give feedback.
-
One gotcha with this implementation - unfortunately it doesn't work when the action has multiple associated values as tuples are not equatable. |
Beta Was this translation helpful? Give feedback.
-
I had a thought that multiple associated values could be handled with a number of overloads but I can't seem to get it to pick the right overload - any ideas on if this can be made to work? public func receive<ValueOne: Equatable, ValueTwo: Equatable>(
_ actionCase: CaseKeyPath<Action, (ValueOne, ValueTwo)>,
with expectedActionValue: (ValueOne, ValueTwo),
timeout duration: Duration = .zero,
assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil,
file: StaticString = #file,
line: UInt = #line
) async where State: Equatable, Action: CasePathable {
await self.receive(
{ action in
guard let actualValue = action[case: actionCase] else {
return false
}
return
actualValue.0 == expectedActionValue.0 &&
actualValue.1 == expectedActionValue.1
},
timeout: duration,
assert: updateStateToExpectedResult,
file: file,
line: line
)
}
public func receive<ValueOne: Equatable, ValueTwo: Equatable, ValueThree: Equatable>(
_ actionCase: CaseKeyPath<Action, (ValueOne, ValueTwo, ValueThree)>,
with expectedActionValue: (ValueOne, ValueTwo, ValueThree),
timeout duration: Duration = .zero,
assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil,
file: StaticString = #file,
line: UInt = #line
) async where State: Equatable, Action: CasePathable {
await self.receive(
{ action in
guard let actualValue = action[case: actionCase] else {
return false
}
return
actualValue.0 == expectedActionValue.0 &&
actualValue.1 == expectedActionValue.1 &&
actualValue.2 == expectedActionValue.2
},
timeout: duration,
assert: updateStateToExpectedResult,
file: file,
line: line
)
} |
Beta Was this translation helpful? Give feedback.
-
Continuing to iterate on this - for now, I've come up with an additional overload for non-equatable values that takes a matcher closure: public func receive<Value>(
_ actionCase: CaseKeyPath<Action, Value>,
matching valueMatcher: (Value) -> Bool,
timeout duration: Duration = .zero,
assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil,
file: StaticString = #file,
line: UInt = #line
) async where State: Equatable, Action: CasePathable {
await self.receive(
{ action in
guard let actualValue = action[case: actionCase] else {
return false
}
return valueMatcher(actualValue)
},
timeout: duration,
assert: updateStateToExpectedResult,
file: file,
line: line
)
} This version can also be used for delegate actions that have multiple values as Swift will automatically destructure the tuple of arguments for you: await store.receive(\.delegate.actionWithMultipleValues) { valueOne, valueTwo in
// perform boolean test here
} In fact, we can re-implement the original version for equatable values in terms of this one: public func receive<Value>(
_ actionCase: CaseKeyPath<Action, Value>,
with expectedActionValue: Value,
timeout duration: Duration = .zero,
assert updateStateToExpectedResult: ((_ state: inout State) throws -> Void)? = nil,
file: StaticString = #file,
line: UInt = #line
) async where State: Equatable, Action: CasePathable, Value: Equatable {
await self.receive(
actionCase,
matching: { $0 == expectedActionValue },
timeout: duration,
assert: updateStateToExpectedResult,
file: file,
line: line
)
} |
Beta Was this translation helpful? Give feedback.
-
We hit the same problem, and it would be nice to have a built-in solution for this. Thanks for sharing the workaround @lukeredpath . cc @mbrandonw |
Beta Was this translation helpful? Give feedback.
-
I'm currently upgrading to 1.4 and starting to take advantage of case key paths - I like the new
TestStore.receive
that takes a case key path as it is generally a lot more concise and most of the time the full action value is not needed as the state assertion is the one that's important.However, I do strongly feel there is a notable exception to this and that is delegate actions - delegate actions are specifically designed to communicate with a parent and sometimes they do this by passing some value up to the parent in the delegate action's associated value. There is no meaningful way of testing these actions with a state assertion as they do not affect the state of the feature under test, but it is often important to assert that the delegate action contains the correct value.
TCA currently offers no mechanism to test this besides the
.receive
overload that takes an(Action) -> Bool
closure. The only other approach is to keep using the old method of testing against the full action however this requires that a) yourAction
type remainsEquatable
which means b) not being able to replace the soft-deprecatedTaskResult
withResult
.I would like an API along the lines of
store.receive(\.delegate.foo, with: fooValue)
and it is possible to build such a helper on top of the built-inreceive
function:This works reasonably well, although the only error you can get is that an unexpected action was received. If this was built-in, it could probably handle the case where the action matches the case key path but the value does not, and provide a specific error message for this scenario (and probably just a diff of the value rather than the entire action).
Beta Was this translation helpful? Give feedback.
All reactions