-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
Description
Version
v18.16.0
Platform
Linux tester 5.15.0-75-generic #82-Ubuntu SMP Tue Jun 6 23:10:23 UTC 2023 x86_64 GNU/Linux
Subsystem
test_runner
What steps will reproduce the bug?
test("mytest", async (t) => {
t.after(async () => { console.log("this is declared first"); });
t.after(async () => { console.log("this is declared next"); });
t.after(async () => { console.log("this is declared last"); });
});How often does it reproduce? Is there a required condition?
Always.
What is the expected behavior? Why is that the expected behavior?
I would expect the after functions to be executed in reverse order:
ℹ this is declared last
ℹ this is declared next
ℹ this is declared first
✔ mytest (3.16807ms)
What do you see instead?
ℹ this is declared first
ℹ this is declared next
ℹ this is declared last
✔ mytest (3.16807ms)
Additional information
Various frameworks provide setup / teardown before and after a test. Usually its desired to perform setup in declared order but teardown in reverse order. This is very convenient because there maybe inter-dependencies. Consider the contrived example of:
- Create browser instance.
- Create incognito context.
- Create browser tab.
- Do tests inside tab.
Once the test ends, regardless of success/failure these should then be torn down in reverse order:
- Destroy browser tab.
- Destroy incognito context.
- Destroy browser instance.
If after() was executed in reverse order, setup and teardown could be conveniently written as below, providing clean teardown regardless at which point setup failed:
test("mytest", async (t) => {
const browser = await create_browser_instance();
t.after(() => destroy_browser_instance(browser));
const context = await create_incognito_context(browser); // if this fails, browser will still be terminated.
t.after(() => destroy_incognito_context(context));
const tab = await create_browser_tab(context); // if this fails, context will still be destroyed and browser terminated.
t.after(() => destroy_browser_tab(tab));
// your test here
});
However, not only is after() executed in declared order, subsequent after() are skipped entirely. I also discovered after() eating the reported error, leaving a very confused developer as to why his test failed with nothing but this as output:
▶ Mytest
✔ subtest 1 (17.906258ms)
✔ subtest 2 (32.88647ms)
✔ subtest 3 (21.395154ms)
✔ subtest 4 (27.778948ms)
▶ Mytest (242.726795ms) <-- Red triangle, nothing else.
ℹ tests 1
ℹ pass 0
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 361.519968
It took a lot of digging to figure out that the first after() was trying to destroy something that was still in use.
To perform teardown in the correct order I'll have to do it manually:
test("mytest", async (t) => {
const ctx = {};
t.after(async () => {
if (ctx.tab) {
await destroy_browser_tab(ctx.tab);
}
if (ctx.context) {
await destroy_incognito_context(ctx.context);
}
if (ctx.browser) {
await destroy_browser_instance(ctx.browser);
}
});
ctx.browser = await create_browser_instance();
ctx.context = await create_incognito_context(ctx.browser);
ctx.tab = await create_browser_tab(ctx.context);
// your test here
});Which makes me wonder why even bother with after() since try {} catch {} finally {} provides the same functionality.