-
Notifications
You must be signed in to change notification settings - Fork 8.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
Change Jest preset to "node" for server-side UTs #133017
Change Jest preset to "node" for server-side UTs #133017
Conversation
Pinging @elastic/kibana-core (Team:Core) |
const subscription = setupContract.esNodesCompatibility$.subscribe(async () => { | ||
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1); | ||
await delay(10); | ||
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2); | ||
|
||
await elasticsearchService.stop(); | ||
await delay(100); | ||
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2); | ||
subscription.unsubscribe(); | ||
done(); | ||
}); |
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.
The .subscribe(next)
callback doesn't support promises, so the promise created by the async function passed to subscribe()
will be ignored, triggering a unhandled promise rejection and process termination if these assertions ever fails To make sure that errors produced by expect()
are propagated properly you should keep assertions in the observable using the proper operator. In this case, concatMap
should be used because it will serially call the passed function for each value produced by setupContract.esNodesCompatibility$
, wait for the promise to resolve, then produce the resolved value on the observable (undefined in this case because the async function has no return value), which will then trigger the firstValueFrom()
helper and cause the observable to be unsubscribed.
const subscription = setupContract.esNodesCompatibility$.subscribe(async () => { | |
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1); | |
await delay(10); | |
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2); | |
await elasticsearchService.stop(); | |
await delay(100); | |
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2); | |
subscription.unsubscribe(); | |
done(); | |
}); | |
await firstValueFrom( | |
setupContract.esNodesCompatibility$.pipe( | |
Rx.concatMap(async () => { | |
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(1); | |
await delay(10); | |
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2); | |
await elasticsearchService.stop(); | |
await delay(100); | |
expect(mockedClient.nodes.info).toHaveBeenCalledTimes(2); | |
}) | |
) | |
) | |
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.
TIL 😲
done(); | ||
|
||
return new Promise<void>((done) => { | ||
setImmediate(() => { |
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.
The timers/promises
module has a promisified version of setImmediate
you could use https://nodejs.org/api/timers.html#timerspromisessetimmediatevalue-options
Like my previous comment, since setImmediate()
executes the callback in a separate callstack the error wouldn't propogate properly and would become unhandled, killing the process if this assertion ever failed so using the promisified version of setImmediate
is actually important here.
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 probably missing the implications of changing the jest preset, so I have some questions:
src/core/server/http/http_service.ts
Outdated
private isListening(): boolean { | ||
return this.httpServer?.isListening?.(); | ||
} |
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.
Why was this addition necessary (and why the optional chaining given this.httpServer
is not optional)?
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.
Alright, that's an interesting one. After some research I think I understood what's happening:
The following test in http_service_test.ts
is failing as soon as we switch to the "node" preset:
test('register preboot route handler on setup', ...)
It turns out that this test is failing cause the mocked httpServer
is not mocking the isListening
method.
So, the fix is simple: we can add a new isListening: jest.fn()
to the members of the mocked class.
The interesting question here is: Why was it NOT failing with the jsdom/browser-like preset?
Here goes my theory:
According to this GH thread:
RxJS catches the error thrown from the subscriber and rethrows it in the next tick.
This behavior has been in place since at least RxJs 6.0.0, I believe.
You can see this by waiting one tick yourself before finishing the test:
I believe
- (A) with jest “jsdom” => all async ops are cancelled / aborted automatically when tests finish.
- (B) with jest “node” => all async operations are awaited for before finishing the tests.
which makes the test (A) succeed, ignoring the RxJS error about to be reported, and gives test (B) enough time to fail.
TLDR: I will fix the test instead of changing prod code.
return infoPromise | ||
? from(infoPromise).pipe( |
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.
Why this check? client.nodes.info
always return a value. Are we adapting production code to adapt for testing environment rather than the other way around?
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.
If I recall correctly, at some point during an integration test, the client.nodes.info()
was returning undefined
instead of a Promise. I don't remember the exact scenario but that's the reason I changed this. Perhaps it is linked to the fact that we start and stop servers very quickly for some tests, not giving this request enough time to complete.
I'll rollback this change to expose the error again.
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.
UPDATE: No error anywhere 🤷🏼♂️ , so I guess we can discard the change!
}); | ||
}); | ||
|
||
describe('#start', () => { | ||
afterEach(async () => await elasticsearchService?.stop()); |
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.
Should we put that at the top-level rather that in the start
block?
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.
Well, there are some #setup tests that don't even start()
the ES service, and the #stop ones that are already calling stop()
, so for those it is actually not needed.
I've just tried to move the stop()
to the top-level as you suggest, and it does not do any harm. I'll update the code, this way we'll make sure we always call it.
💛 Build succeeded, but was flakyFailed CI StepsTest Failures
Metrics [docs]
History
To update your PR or re-run it, just comment with: |
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.
LGTM
* change Jest preset to "node" for server-side UTs * small code enhancements, graceful stop (cherry picked from commit 0052fd2)
💚 All backports created successfully
Note: Successful backport PRs will be merged automatically after passing CI. Questions ?Please refer to the Backport tool documentation |
* change Jest preset to "node" for server-side UTs * small code enhancements, graceful stop (cherry picked from commit 0052fd2) Co-authored-by: Gerard Soldevila <[email protected]>
Summary
Part of the spacetime-511 initiative. We are updating Jest preset for server-side unit tests, switching from using a "browser-like" environment to a "node-like" one. This makes the testing environment match better the production one.
The idea is to switch the Jest preset for integration tests as well, but doing so breaks the build, as some tests remain hung awaiting for the logic to finish all async tasks. Thus, in order to have a graceful shutdown, we must first make sure there are no loose ends on the
stop()
lifecycle of the various services and plugins that shape Kibana.Checklist