Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds isAborted method and joiners array property to mock task #2297

Merged
merged 6 commits into from
Jun 27, 2022
Merged

Adds isAborted method and joiners array property to mock task #2297

merged 6 commits into from
Jun 27, 2022

Conversation

lourd
Copy link
Contributor

@lourd lourd commented May 13, 2022

Q A
Fixed Issues? Fixes #2205
Patch: Bug Fix? 馃憤
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? No new tests
Any Dependency Changes?

When doing saga integration testing (using expectSaga from redux-saga-test-plan), creating a mock task (passed into the saga using the fork.fn static providers) that's passed to a real join effect throws an error. Adding isAborted and the joiners array are necessary for it to not throw an error.

During code review, it was determined that createMockTask should have an API more similar to the real task implementation. setRunning was deprecated, and trying to change the state of a task more than once will now throw an error.

Also includes new unit tests for covering basic usage, and a small update to the docs with slightly improved instructions.

@changeset-bot
Copy link

changeset-bot bot commented May 13, 2022

馃 Changeset detected

Latest commit: 9cc6729

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@redux-saga/testing-utils Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

let _result
let _error

return {
[TASK]: true,
isRunning: () => _isRunning,
isAborted: () => _isAborted,
Copy link
Member

Choose a reason for hiding this comment

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

This always returns a constant value - false. Shouldn't we expose a similar setter as we do for other "statuses" (setRunning, setError, etc)? I also wonder - some of those can't be true at the same time, so I think that it would make sense to create an internal status variable, initialize it to running' and then switch it appropriately based on those setters. That way we won't end up with an impossible task with t.isRunning() === true && t.isAborted() === true

Copy link
Member

Choose a reason for hiding this comment

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

In general, it's fairly easy to inspect how the real task behaves here:

isRunning: () => status === RUNNING,
/*
This method is used both for answering the cancellation status of the task and answering for CANCELLED effects.
In most cases, the cancellation of a task propagates to all its unfinished children (including
all forked tasks and the mainTask), so a naive implementation of this method would be:
`() => status === CANCELLED || mainTask.status === CANCELLED`
But there are cases that the task is aborted by an error and the abortion caused the mainTask to be cancelled.
In such cases, the task is supposed to be aborted rather than cancelled, however the above naive implementation
would return true for `task.isCancelled()`. So we need make sure that the task is running before accessing
mainTask.status.
There are cases that the task is cancelled when the mainTask is done (the task is waiting for forked children
when cancellation occurs). In such cases, you may wonder `yield io.cancelled()` would return true because
`status === CANCELLED` holds, and which is wrong. However, after the mainTask is done, the iterator cannot yield
any further effects, so we can ignore such cases.
See discussions in #1704
*/
isCancelled: () => status === CANCELLED || (status === RUNNING && mainTask.status === CANCELLED),
isAborted: () => status === ABORTED,
result: () => taskResult,
error: () => taskError,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, good point; updated the PR with your suggestion.

@lourd lourd requested a review from Andarist June 6, 2022 13:27
@@ -107,7 +107,8 @@ export interface SagaIteratorClone extends SagaIterator {
export function createMockTask(): MockTask

export interface MockTask extends Task {
setRunning(running: boolean): void
setRunning(): void
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The refactor to a status enum required changing this type signature.

Copy link
Member

Choose a reason for hiding this comment

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

We could add a @deprecated signature back and handle both at the implementation level - task.setRunning() should just be synonymous with task.setRunning(true). I'm not 100% sure what task.setRunning(false) should correspond to though... it seems like canceled would fit better here than aborted

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For all of the times I've used this in the past, I've always had to do:

mockTask.setResult(X)
mockTask.setRunning(false)

This is necessary to get the rest of the saga to correctly proceed when joining on a mock task.

I've never had to do setRunning(true) because the tasks are already running when created.

I think taking out setRunning altogether is simpler than trying to guess what people would expect. Maybe that means publishing another major version? I think since it's a testing utility if we did it in a minor version it would be acceptable.

setRunning: b => (_isRunning = b),
setResult: r => (_result = r),
setError: e => (_error = e),
setRunning: () => (status = RUNNING),
Copy link
Member

Choose a reason for hiding this comment

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

just a random thought without full analysis - perhaps setRunning should just be deprecated? it seems to me that making a mock task forcefully take a transition that isn't possible in the real code is asking for trouble.

Valid transitions are those:

  • running -> done (can be achieved by calling setResult)
  • running -> aborted (can be done by calling setError)
  • running -> canceled (could be done by cancel)

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I think that's a good idea.

Went ahead and added checks for that and tests to cover it :)

@lourd lourd requested a review from Andarist June 11, 2022 19:40
@lourd
Copy link
Contributor Author

lourd commented Jun 11, 2022

@Andarist addressed all your comments and updated the PR description, PTAL.

@@ -107,7 +107,6 @@ export interface SagaIteratorClone extends SagaIterator {
export function createMockTask(): MockTask

export interface MockTask extends Task {
setRunning(running: boolean): void
Copy link
Member

Choose a reason for hiding this comment

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

I totally agree with this change - I am concerned about this being a breaking change though. I'm not opposed to releasing a new major version of the Redux Saga or accompanying packages but this seems like a minor cleanup that doesn't necessarily require us to immediately bump a major version.

This PR will probably change the behavior of this task mock slightly, one way or another. But removing a known method is a big breaking change (for this package alone). Could we think about something less disruptive here? My preference would be to add this method back and introduce @deprecated + console.warn for what we want to phase out.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, re-added with a deprecation warning.

@lourd lourd requested a review from Andarist June 14, 2022 13:20
let _isRunning = true
let _result
let _error
let status = RUNNING
Copy link
Member

Choose a reason for hiding this comment

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

nit: in general, it feels that keeping this as a number doesn't buy us anything here since we have to remap this later to string anyway when throwing an error. We could just ditch that if we'd use string status here.

I don't feel strongly about this though - both approaches are fine.

assertStatusRunning(status)
status = CANCELLED
},
joiners: [],
Copy link
Member

Choose a reason for hiding this comment

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

I have actually never used this mock task myself - it was here when I got involved in the project and I never bothered to tweak/remove it. I'm also very rusty when it comes to this codebase as I wasn't actively using it for a couple of last years. I wonder though - how useful this mock is in its current shape.

It seems to me that this now won't throw when passed to a join but at the same time the join effect will never get resolved.

I wonder if setError works OK when it comes to aborting the parent task etc. Do we have any tests for that? Does it work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of the new tests in this PR are pretty comprehensive IMO of describing what behavior works vs what doesn't. If you set the error before it's passed to a join effect it will correctly throw and abort the parent task.

Copy link
Member

@Andarist Andarist left a comment

Choose a reason for hiding this comment

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

Just a changeset away from landing this :)

@neurosnap neurosnap merged commit fdca798 into redux-saga:master Jun 27, 2022
@lourd lourd deleted the update-mock-task branch June 27, 2022 16:02
This was referenced Aug 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

createMockTask does not properly mock task
3 participants