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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rules): add no-test-callback rule #179

Merged
merged 6 commits into from Oct 22, 2018
Merged

feat(rules): add no-test-callback rule #179

merged 6 commits into from Oct 22, 2018

Conversation

SimenB
Copy link
Member

@SimenB SimenB commented Oct 21, 2018

Using a done callback is old-school, all the cool kids should use Promises these days. Especially because of async-await. We might also want to remove done from a future version of Jest, and this is a nice start to make people migrate.

/cc @rubennorte, since we talked about something like this

@SimenB SimenB requested a review from macklinu October 21, 2018 18:32
Copy link
Contributor

@ranyitz ranyitz left a comment

Choose a reason for hiding this comment

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

This is awesome!

docs/rules/no-test-callback.md Outdated Show resolved Hide resolved
SimenB and others added 2 commits October 21, 2018 21:10
Co-Authored-By: SimenB <sbekkhus91@gmail.com>
context.report({
node: argument,
message: 'Illegal usage of test callback',
fix(fixer) {
Copy link
Member

Choose a reason for hiding this comment

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

❤️

},
],
output:
'test("something", async () => {await new Promise(done => {done();})})',
Copy link
Member

Choose a reason for hiding this comment

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

a test with a bit more complex code would be nice to validate fixer

Copy link
Member Author

Choose a reason for hiding this comment

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

Feel free to push some examples 🙂 Or write them here, and I can add them

@rubennorte
Copy link

@SimenB looks very good! :D

3 similar comments
@rubennorte
Copy link

@SimenB looks very good! :D

@rubennorte
Copy link

@SimenB looks very good! :D

@rubennorte
Copy link

@SimenB looks very good! :D

@rubennorte
Copy link

@SimenB looks good! :D

Copy link
Collaborator

@macklinu macklinu left a comment

Choose a reason for hiding this comment

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

Good idea for a rule! 👍

rules/__tests__/no-test-callback.test.js Outdated Show resolved Hide resolved
test('some test', () => {
return new Promise(done => {
expect(false).toBe(true);
done();
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think wrapping this code in a Promise as a fixer is a good first step and a consistent way to wrap code in the "return a Promise" paradigm. Do we also want to document using .resolves, .rejects or returning a Promise / await statement as other suggested alternatives?

Copy link
Member Author

@SimenB SimenB Oct 22, 2018

Choose a reason for hiding this comment

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

The thinking is that returning a promise is a safe change, as it will always work (I think?). Messing with the body of the tests are both way harder and more brittle. The current solution also makes no assumptions about assertions.

Regarding resolve and rejects, I actually want to remove them from Jest and ask people to use async-await. That's a whole other can of worms, though 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

oh, I now understood that you meant the docs... I thought you talked about the fixer, sorry!

Yeah, we should definitely include some prose about using at least async-wait over returning the promise (async functions returns promises implicitly, so it's almost always a better idea anyways). Mentioning resolves and rejects (especially rejects as resolves i better handled by await) is probably a good idea as well.

Would you mind sending a PR for it? 🙏

Choose a reason for hiding this comment

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

@SimenB rejects is very useful to avoid using logic in try/catch blocks (like .toThrow but for async functions), so I would keep it (and resolves for consistency) unless we come up with an equally good alternative.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, for sure.

What I want is same as ava (which returns the error from t.throws, but I'm not sure if that makes sense for expect

Choose a reason for hiding this comment

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

I just saw that ava has throwsAsync for that :D

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, new in v1, it was just throws before. But yeah, I like that pattern. Doesn't fit into expect, though

rules/no-test-callback.js Show resolved Hide resolved
Co-Authored-By: SimenB <sbekkhus91@gmail.com>
@SimenB
Copy link
Member Author

SimenB commented Oct 22, 2018

I've ran this on the Jest codebase without it crashing, or reporting false positives, BTW. So should be fairly good. I'd love for people to try it out though!

@SimenB SimenB merged commit 7344607 into master Oct 22, 2018
@SimenB SimenB deleted the no-test-callback branch October 22, 2018 22:53
@SimenB
Copy link
Member Author

SimenB commented Oct 22, 2018

🎉 This PR is included in version 21.26.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@ron23
Copy link

ron23 commented Feb 8, 2020

Hey @SimenB Thanks for this rule! It's great and I wanna be part of the cool kids using as/aw.
I have a question about the --fix though: It seems like it's wrapping the done arg with a promise, (basically using the done as a resolver) but I don't think it really needs to.
Why not simply remove the done argument all together and add a return? Is that much more complicated?

This is my original test:

    it('should work', done => {
      mockApi.onGet(`/account/someone`).reply(200, {});
      return Api.fetch(someone).then(result => {
        expect(result).toEqual([]);
        done();
      });
    });

And this is that the fix did:

    it('should work', () => {return new Promise(done => { => {
      mockApi.onGet(`/account/someone`).reply(200, {});
      return Api.fetch(someone).then(result => {
        expect(result).toEqual([]);
        done();
      });
    });

And this is what I want it to be:

    it('should work', () => {
      mockApi.onGet(`/account/someone`).reply(200, {});
      return Api.fetch(someone).then(result => {
        expect(result).toEqual([]);
      });
    });

Sorry for bumping old thread. If you want I can open a new ticket for it.

@G-Rath
Copy link
Collaborator

G-Rath commented Feb 8, 2020

@ron23 this is because of the complexity around doing that, specifically finding the right done:

it('should work', done => {
  mockApi.onGet(`/account/someone`).reply(200, {});

  return new Promise(done => {
    doSomething();
    done();
  }).then(() =>
    Api.fetch(someone).then(result => {
      expect(result).toEqual([]);
      done();
    })
  );
});

In the above, how do you identify the done that should be removed? :)

To be honest, this rule should probably provide a suggestion rather than a fix :/

@SimenB
Copy link
Member Author

SimenB commented Feb 9, 2020

Ideally your original example should actually be an error since you're both returning a promise and using a done callback. Mocha actually errors in this case - Jest really should do the same. Using both methods of signalling a test as asynchronous is usually an error, and it's definitely confusing.

So if we say you only use the done callback and don't return the promise (which is the intended way):

it('should work', done => {
  mockApi.onGet(`/account/someone`).reply(200, {});
  Api.fetch(someone).then(result => {
    expect(result).toEqual([]);
    done();
  });
});

Then the only (relatively) safe transform we can do is wrapping the whole thing in a promise and replacing the done call with resolve from the promise executor.

Ideally, you'd just return the promise as you suggest, though, and have no new Promise wrapper 👍

To be honest, this rule should probably provide a suggestion rather than a fix :/

This rule predates suggestions existing in eslint by a year. But yes, I agree - this is not really a safe fix to apply in all cases

@ron23
Copy link

ron23 commented Feb 9, 2020

Thanks @SimenB and @G-Rath . Good stuff!
I used find & replace and ended up refactoring all of the tests that had that format. I can now safety enable the rule to prevent people from using done!

@vvo
Copy link

vvo commented Mar 9, 2020

Hey @SimenB I just got warned about this, then I tested the example you gave that should timeout and it does not times out, it just show the error:

https://codesandbox.io/s/jest-example-lcpkg

image

Could you precise maybe in the documentation what's really wrong with this pattern? Like with a use case involving maybe a time where it will really timeout? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants