Skip to content

✨ Support for beforeEach/afterEach in @fast-check/vitest - #6587

Closed
rushelex wants to merge 17 commits into
dubzzz:mainfrom
rushelex:rushelex/vitest-hooks
Closed

✨ Support for beforeEach/afterEach in @fast-check/vitest#6587
rushelex wants to merge 17 commits into
dubzzz:mainfrom
rushelex:rushelex/vitest-hooks

Conversation

@rushelex

Copy link
Copy Markdown
Contributor

Description

This PR closes issue #3942.

This PR related to Jest PR #6586.

Currently, when using it.prop / test.prop in @fast-check/vitest, beforeEach and afterEach hooks are only called once per test — not once per property run. This means that if a property executes 100 runs inside a single it.prop, hooks like beforeEach (commonly used for setup/teardown, mocking, DB transactions, etc.) only fire once instead of 100 times.

This PR makes beforeEach/afterEach fire for each property run, not just once per test. For example, with 1 regular it() + 1 regular it() + 1 it.prop() with numRuns: 10, beforeEach/afterEach will now fire 12 times total instead of 3.

Strategy:

  • The test framework calls its own beforeEach before the test (covers 1st run)
  • Between consecutive property runs: call framework's afterEachbeforeEach via propertyInstance.beforeEach() hook
  • The test framework calls its own afterEach after the test (covers last run)
  • Result: exactly N beforeEach + N afterEach for N property runs

@fast-check/vitest: Uses vitest internals (getCurrentSuite(), getHooks()) to collect hooks from the suite chain. Collects cleanup functions returned by beforeEach hooks and executes them before afterEach hooks between runs, matching vitest's cleanup-before-afterEach execution order.

ChecklistDon't delete this checklist and make sure you do the following before opening the PR

  • The name of my PR follows gitmoji specification
  • My PR references one of several related issues (if any)
    • New features or breaking changes must come with an associated Issue or Discussion
    • My PR does not add any new dependency without an associated Issue or Discussion
  • My PR includes bumps details, please run pnpm run bump and flag the impacts properly
  • My PR adds relevant tests and they would have failed without my PR (when applicable)

Advanced

  • Category: ✨ Introduce new features
  • Impacts: Generated values: no impact. Shrink values: no impact. Performance: minimal overhead from hook collection and invocation between property runs. Typings: no user-facing type changes.

- Add explicit undefined checks for test and suite
- Replace truthy checks with explicit undefined comparisons
- Use nullish coalescing operator (??) instead of logical OR
- Simplify afterEach hook reversal logic
- Add whitespace for better code readability
- Add comment explaining undefined test/suite cases
- Replace while(current) with while(true) and explicit termination
- Add type annotation for current variable
- Add comment explaining suite chain traversal logic
- Clarify that loop terminates at File node with filepath
- Collect cleanup functions returned by beforeEach hooks
- Execute cleanups before afterEach hooks between runs
- Run remaining cleanups after final property iteration
- Match vitest's cleanup-before-afterEach execution order
@rushelex
rushelex requested a review from dubzzz as a code owner February 10, 2026 15:45
@changeset-bot

changeset-bot Bot commented Feb 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7e308e8

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

This PR includes changesets to release 1 package
Name Type
@fast-check/vitest Minor

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

Comment thread packages/vitest/test/vitest-fast-check.spec.ts Outdated
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated

@dubzzz dubzzz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Overall the PR looks good to me. I just did a quick pass on it. Starting with vitest one but comments would probably be the same in the other one. Let's wait for this one to be validated and merged before replicating the same changes on the other so that I don't make you lose time with fixing both at the same time.

Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated
Comment on lines 170 to 177
// Run remaining cleanup functions from the last manual beforeEach call.
// Vitest supports returning a cleanup function from beforeEach; the first
// iteration's cleanup is handled by vitest's own afterEach lifecycle, but
// cleanups from iterations 2..N are captured and invoked by us.
for (let i = pendingCleanups.length - 1; i >= 0; i--) {
await pendingCleanups[i]();
}
},

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

So if I get it well, clean-ups of the first beforeEach will be executed at the end by vitest. This manual clean-up is cleaning the last run within fast-check (if there was one not being the first).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Said differently we might have:

  • beforeEach#1 done by vitest
  • beforeEach#2 done by beforeEach of fast-check
  • cleanUp of beforeEach#2 done by beforeEach of fast-check
  • beforeEach#3 done by beforeEach of fast-check
  • cleanUp of beforeEach#3 done by the manual clean-up loop after assert
  • cleanUp of beforeEach#1 done by vitest

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, you're right. Cleanup#1 is held by vitest in a local variable of runTest and runs after vitest's own afterEach — we cannot intercept it.

@rushelex
rushelex requested a review from dubzzz February 16, 2026 17:55
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts
});

it.concurrent(
`should never call beforeEach/afterEach twice per run in ${runnerName}.prop`,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Actually the test does not really does that. This one mostly checks that we don't have a too large diff between number of beforeEach runs vs number of afterEach runs

},
);

it.concurrent(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

IMO no real need to check for shrinking vs no-shrinking. Having the no-shrinking cover is good enough for this feature. So worth dropping this test case

},
);

it.concurrent(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'd rather group the two last tests into one doing something like:

Have an array being empty.
In beforeEach, push "beforeEach('runId')" in the array.
In run, push "predicate('runId')" in the array.
In clean-up of beforeEach, push "clean-up('runId')" in the array.
In afterEach, push "afterEach('runId')" in the array.

Maybe limit yourself to 3 runs and in afterAll expect an hardcoded array. Such test would have the benefit to capture all remaining things including relative ordering of calls and also the thing around clean-up of 1st run not being called exactly when it should be.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Regarding my comment https://github.com/dubzzz/fast-check/pull/6587/changes#r2819152307, if we address it (maybe in another PR) it would be great to have a test with the same idea as the one described above but in the context of a failure. This time no way to hardcode the output but we could in the afterAll test its shape.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@claude could you try to take that one? one a PR doing the fix I'm asking

Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts
// fc's beforeEach hook above. Cleanup#1 is held by vitest in a local
// variable of runTest and runs after vitest's own afterEach — we cannot
// intercept it.
for (let i = pendingCleanups.length - 1; i >= 0; i--) {

@dubzzz dubzzz Feb 17, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

With this trick, I have the feeling that we might not support correctly things such as:

beforeEach(() => {
  console.log("before");
});

test("abc", () => {
  console.log("test");
  throw new Error("a");
});

afterEach(() => {
  console.log("after");
});

In such context vitest prints the three logs. In our case if fc.assert fails, I feel that we might not execute clean-ups properly...

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Not blocking at all for the first iteration on this feature

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@claude could you try to take that one? one a PR doing the fix I'm asking

rushelex and others added 5 commits February 26, 2026 22:58
Co-authored-by: Nicolas DUBIEN <github@dubien.org>
Co-authored-by: Nicolas DUBIEN <github@dubien.org>
Co-authored-by: Nicolas DUBIEN <github@dubien.org>
Co-authored-by: Nicolas DUBIEN <github@dubien.org>
Co-authored-by: Nicolas DUBIEN <github@dubien.org>
@dubzzz dubzzz changed the title 🪝 Support for beforeEach/afterEach in @fast-check/vitest ✨ Support for beforeEach/afterEach in @fast-check/vitest Mar 7, 2026
@dubzzz

dubzzz commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Let's go for it! Thanks for the contribution.

I'm gonna address on separate PRs the remaining points.

@pkg-pr-new

pkg-pr-new Bot commented Mar 7, 2026

Copy link
Copy Markdown
@fast-check/ava

npm i https://pkg.pr.new/@fast-check/ava@6587

fast-check

npm i https://pkg.pr.new/fast-check@6587

@fast-check/jest

npm i https://pkg.pr.new/@fast-check/jest@6587

@fast-check/packaged

npm i https://pkg.pr.new/@fast-check/packaged@6587

@fast-check/poisoning

npm i https://pkg.pr.new/@fast-check/poisoning@6587

@fast-check/vitest

npm i https://pkg.pr.new/@fast-check/vitest@6587

@fast-check/worker

npm i https://pkg.pr.new/@fast-check/worker@6587

commit: 0d1164f

@codecov

codecov Bot commented Mar 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.72%. Comparing base (5638730) to head (7e308e8).
⚠️ Report is 83 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6587      +/-   ##
==========================================
- Coverage   94.72%   94.72%   -0.01%     
==========================================
  Files         209      209              
  Lines        5670     5666       -4     
  Branches     1502     1500       -2     
==========================================
- Hits         5371     5367       -4     
  Misses        286      286              
  Partials       13       13              
Flag Coverage Δ
tests 94.72% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dubzzz

dubzzz commented Mar 7, 2026

Copy link
Copy Markdown
Owner

@rushelex I had to recreate the PR to deal with last few blockers and totally unlock it. The new PR is accessible at #6695. No worry you'll still be marked as author on the finally merged PR and still be added as a contributor to the repository.

My plan is to address the few last TODOs: tests and bugs in term of errors before merging it.

dubzzz added a commit that referenced this pull request Mar 7, 2026
## Description

<!-- Describe your change and explain what this PR is trying to solve
-->

Original PR with full description available at #6587.

Fixes #3942

<!-- Add any additional context here -->

## Checklist

— _Don't delete this checklist and make sure you do the following before
opening the PR_

- [x] I have a full understanding of every line in this PR — whether the
code was hand-written, AI-generated, copied from external sources or
produced by any other tool
- [x] I flagged the impact of my change (minor / patch / major) either
by running `pnpm run bump` or by following the instructions from the
changeset bot
- [x] I kept this PR focused on a single concern and did not bundle
unrelated changes
- [x] I followed the [gitmoji](https://gitmoji.dev/) specification for
the name of the PR, including the package scope (e.g. `🐛(vitest)
Something...`) when the change targets a package other than `fast-check`
- [x] I added relevant tests and they would have failed without my PR
(when applicable)

<!-- PRs not checking all the boxes may take longer before being
reviewed -->
<!-- More about contributing at
https://github.com/dubzzz/fast-check/blob/main/CONTRIBUTING.md -->

---------

Co-authored-by: Aleksey Shelementev <rushelex@gmail.com>
@dubzzz

dubzzz commented Mar 7, 2026

Copy link
Copy Markdown
Owner

Closing as already merged

@dubzzz dubzzz closed this Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants