-
Notifications
You must be signed in to change notification settings - Fork 47.3k
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
toWarnInDev matcher; throw on unexpected console.error #11786
Conversation
scripts/jest/matchers/toWarnDev.js
Outdated
if (__DEV__) { | ||
if (typeof expectedWarnings === 'string') { | ||
expectedWarnings = [expectedWarnings]; | ||
} else if (!Array.isArray(expectedWarnings)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bvaughn Are you planning on using Flow inside tests at some point? If so, you could get rid of these checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True.
And no idea. 😄
]); | ||
|
||
// Should not log any additional warnings | ||
expect(() => React.createElement('div')).toWarnDev([]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary? Shouldn’t an unexpected log fail the test anyway? I expect that if we explicitly mark each warning block then any warning outside should be a failure. Maybe even immediately when it’s called rather than at the end of the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn’t an unexpected log fail the test anyway
Good point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I look at this again, I believe this explicit check is necessary since we've necessarily spied on console.error
in order for the matcher to work.
IIR, I originally tried to clear the spy at the end of the matcher, so unexpected errors/warnings would cause an error, but neither the version of Jest we're using (21.3.0-beta.4
) nor the latest release (22) actually expose the spy.mockReset
method the docs mention.
We could call spy.and.callThrough()
at the bounds of the matcher, so that errors were actually passed through, but (a) that could negatively affect tests with multiple .toWarnDev
calls and (b) this wouldn't actually fail the test, only print the red text to the console.
Maybe I'm overlooking something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that I look at this again, I believe this explicit check is necessary since we've necessarily spied on console.error in order for the matcher to work.
Wouldn't this approach solve it? #11786 (comment)
IIR, I originally tried to clear the spy at the end of the matcher, so unexpected errors/warnings would cause an error
I don't quite understand what clearing means in this context, or why it would be helpful.
My proposal would be to set up a mock when we enter toWarn
and tear it down when we exit it. Outside of toWarn
, I'd want console.error
to be replaced with a function that insta-throws.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're saying the same thing, but misunderstanding each other.
My initial design for this matcher was to setup a spy/mock when entering and then remove it before exiting. This way unexpected console.error
calls would fail the test (as they do if you don't spy in the first place). The Jest docs suggest this should be possible via a mockReset
method, but that method doesn't actually exist (in Jest 21 or 22) so I'm not sure why the docs mention it. The problem then, is that once we've spied on console.error
initially, we can't remove that spy and so any unexpected calls to it don't actually cause failures.
We could work around this in other ways. For example, I could not use Jest's spyOn
functionality at all- but just override console.error
with my own function and then restore the originally one before exiting. Maybe that's best anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(My initial comment didn't make sense so I edited it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! I think I understand what you're saying better now.
The boundary is clearer with the new API. Before the boundary was more informal, between spyOn
and the first (...calls.length).toBe()
check.
It was still possible to do the throw-immediate thing before though. That's what confused me a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it was also easier for the old spies to miss things if warnings were logged after the last count check too. That's another advantage of this boundary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was still possible to do the throw-immediate thing before though
How would it work with this test case?
spyOnDev(console, 'error')
console.error('unexpected warning')
console.error('expected warning')
expect(console.error.calls.count).toBe(1)
expect(console.error.calls.argsFor(0)[0]).toBe('expected warning')
Previously we couldn't safely throw on the first console.error
because we didn't know its message is unexpected by the time it ran.
But with toWarn
we can because we know the exhaustive list of expected messages (and their count) before console.error
is called.
Thus we'll get a full stack trace to the place where the message actually fired, as opposed to our check. And less code would run (which aids debugging).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It just occurred to me coming back from lunch that, since I know the error message(s) ahead of time, I could also throw immediately (within my fake spy) any time a message is logged that isn't expected.
I'll rework the matcher to behave this way instead.
@@ -317,6 +270,8 @@ describe('ReactElementValidator', () => { | |||
'or a class/function (for composite components) but got: null.' + | |||
(__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), | |||
); | |||
|
|||
// We can't use .toWarnDev() here because we can't chain it with .toThrowError() | |||
if (__DEV__) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let’s bite the bullet and nest assertions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what you have in mind here. Mind giving a faux code example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect(() => {
expect(() => {
}).toWarnInDev()
}).toThrowError('message')
Although there's a hazard of not specifying the message and thus catching a failure from the inner expect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe nesting them the other way around would work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nesting toWarnDev
inside of toThrowError
won't work. Failures in the toWarnDev
assertion are silent. But nesting it the other way around (toThrowError
inside) does seem to work.
]); | ||
|
||
// Should not error for strings | ||
expect(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same; I would expect any warning that isn’t inside a protected block to fail immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was just me being silly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, I expect that with current infra it fails at the end of the test.
However, I propose that toWarnInDev
acts as a boundary, and we change console.error
/console.warn
to immediately throw outside of that boundary. This way we get the error immediately at the right call site instead of at the end of the test.
…nexpected console.error/warn calls will fail tests.
I've refactored this to fail faster for unexpected warnings. I also added some syntax snippets to the description. Still need to look into the following:
|
It looks like the stack thing I mentioned is intentional. Jest overrides the "real" stack with one that points to the matcher. I've posted to the internal Jest group about this and will follow up about it later. For now, I'll just leave a TODO comment. |
I filed a Jest issue (jestjs/jest/issues/5136) and PR (jestjs/jest/pull/5138) for preserving the call stack for errors inside of a custom matcher. I don't think either is a blocker for moving forward with this issue though, so back to you @gaearon 😄 |
What did we decide about throwing? We should make sure this fails the test: try {
console.error('hi');
} catch (err) { } |
That's a good point, Dan. Using the I think the best compromise might be to track call stacks during execution, save them for later, then throw a single error from the Maybe I'm overdoing this, but here's a compromise that I think might work? For example, this test: describe('Example', () => {
it('should warn about an unexpected log', () => {
const foo = () => bar();
const bar = () => baz();
const baz = () => console.error('unexpected warning');
foo();
})
it('should warn about two unexpected logs', () => {
const foo = () => bar();
const bar = () => baz();
const baz = () => console.error('unexpected warning');
try {
foo();
console.error('another unexpected warning')
} catch (err) { }
})
}); |
I've pushed a fix for this (as described above) and updated the description of this PR for the various cases I've tested (locally) with this matcher. |
I think this makes sense. I'll review this tomorrow. |
Friendly ping 😄 |
Another friendly ping |
Sorry, I was on holidays! |
Of course! 😄 Was just a reminder for when we both came back. |
Get Outlook for iOS
On Tue, Jan 2, 2018 at 8:08 AM -0500, "Dan Abramov" <[email protected]> wrote:
Sorry, I was on holidays!
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.
|
Thanks, but I don't need Outlook 😉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LG to me
scripts/jest/matchers/toWarnDev.js
Outdated
} | ||
|
||
// Fail early for unexpected warnings to preserve the call stack. | ||
throw Error(`Unexpected warning recorded: "${message}"`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we include the expected next warning message if one exists, in this message?
Otherwise it's a bit confusing that simply having a typo in the expected warning prints a message as if a warning was not expected at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scripts/jest/setupTests.js
Outdated
console[methodName] = newMethod; | ||
|
||
env.beforeEach(() => { | ||
newMethod.__callCount = 0; | ||
unexpectedConsoleCallStacks.splice(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I always forget what this does. Maybe = []
or .length = 0
?
How do you feel about converting other tests as part of this PR? I don't care strongly either way. |
I could do that, although this PR is already pretty big. I stopped converting because I was getting increasing concerned about merge conflicts. 😄 I'd be happy to do a follow up PR or two quickly after merging this. I'm willing to do it all as part of this PR too though if you have a preference. |
Let's split (as long as both styles keep working and there's no regressions). |
True. I was mostly worried about having to deal with merge conflicts myself 😅 I'll cause potential conflicts for others no matter what I do. I've merged |
As a stretch goal it’d be nice to highlight the text difference on a character level. I often make small tweaks and then it’s super annoying to stare into the message trying to figure out where exactly the different lies (and which one is right). |
Yeah, I briefly considered doing this, using
Edit: I think |
* Added toWarnInDev matcher and connected to 1 test * Added .toLowPriorityWarnDev() matcher * Reply Jest spy with custom spy. Unregister spy after toWarnDev() so unexpected console.error/warn calls will fail tests. * console warn/error throws immediately in tests by default (if not spied on) * Pass-thru console message before erroring to make it easier to identify * More robustly handle unexpected warnings within try/catch * Error message includes remaining expected warnings in addition to unexpected warning
While writing tests for unsafe async warnings, I noticed that in certain cases, errors were swallowed by the toWarnDev matcher and resulted in confusing test failures. For example, if an error prevented the code being tested from logging an expected warning- the test would fail saying that the warning hadn't been logged rather than reporting the unexpected error. I think a better approach for this is to always treat caught errors as the highest-priority reason for failing a test. I reran all of the test cases for this matcher that I originally ran with PR facebook#11786 and ensured they all still pass.
While writing tests for unsafe async warnings, I noticed that in certain cases, errors were swallowed by the toWarnDev matcher and resulted in confusing test failures. For example, if an error prevented the code being tested from logging an expected warning- the test would fail saying that the warning hadn't been logged rather than reporting the unexpected error. I think a better approach for this is to always treat caught errors as the highest-priority reason for failing a test. I reran all of the test cases for this matcher that I originally ran with PR #11786 and ensured they all still pass.
Relates to #11626.
Added a new matcher for dev errors and warnings. Rather than checking for warnings like this:
You can now write this:
In addition to being syntactically shorter, I've also changed our default
console
overrides to report the correct call-stack for unexpected warnings. The combination of these two changes provides a couple of benefits:calls.count()
could result in suppressed/ignored warnings.I've updated a dozen or so tests to use the new matcher, to make sure it works correctly. There are still several more with
console
spies that I can remove via follow-up PRs.Follow Up Work
Error
s and re-throw asJestAssertionError
s for better stack traces (once Expose JestAssertionError to custom matchers jestjs/jest#5138 is released).console
to use the new matcher.spyOn
helpers to detect if they're being used to spy onconsole
redirect to use the new matcher instead. (See PR Fixed potential false-positive in toWarnDev matcher #11898 for a possible implementation.)toWarnDev
matchers or whether to throw an error if this pattern is detected.Expected failures
Unexpected warnings
Unexpected warnings within a
try
/catch
Errors thrown within
toWarnDev
Invalid
toThrow
withintoWarnDev
Passing
toWarnDev
withintoThrow
Incorrect/non-matching warning
Passing tests
Nested
toThrow
withintoWarnDev
Nested
toWarnDev
withintoThrow
Open Questions
Nested warnings
This is currently supported, although it's unclear if this is a case we would ever actually need or want to use. Once we've migrated the current warning tests over to use this new matcher, if we determine that we don't need to support nesting, we can change it to fail explicitly.