Skip to content

🪝 Support for beforeEach/afterEach in @fast-check/jest - #6586

Open
rushelex wants to merge 8 commits into
dubzzz:mainfrom
rushelex:rushelex/vitest-jest-hooks
Open

🪝 Support for beforeEach/afterEach in @fast-check/jest#6586
rushelex wants to merge 8 commits into
dubzzz:mainfrom
rushelex:rushelex/vitest-jest-hooks

Conversation

@rushelex

@rushelex rushelex commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Description

This PR closes issue #3942.

Currently, when using it.prop / test.prop in @fast-check/jest, 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/jest: Accesses jest-circus internal state via Object.getOwnPropertySymbols(globalThis) matching Symbol('JEST_STATE_SYMBOL') string representation. Only works with jest-circus runner (default since Jest 27); graceful degradation for jest-jasmine2 (hooks fire once as before). Does not work in worker mode (property runs in a separate process without access to jest-circus state).

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 — import type from jest-circus used internally only.

@rushelex
rushelex requested a review from dubzzz as a code owner February 10, 2026 12:06
@changeset-bot

changeset-bot Bot commented Feb 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e92d09a

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/jest 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

}

// Between runs: close previous iteration, then open next
for (const hook of afterEachHooks) {

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.

Why not hooking yourself in afterEach?

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.

By doing so these after don't run with others... but actually I get why you did it

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.

What do you mean? Call propertyInstance.afterEach after jest afterEach (and vitest afterEach)?

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.

but actually I get why you did it

So, what should we do in this place? What do you think? 🙂

propertyInstance.beforeEach(async (previousHook: () => Promise<void>) => {
await previousHook();

if (isFirstRun) {

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.

The same logic goes for last run? Unfortunately we won't be able to know if it's the last or not

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, here's the lifecycle of fc and jest hook calls:

Current pass for N runs:

jeft.beforeEach         ← covers the 1st beforeEach
    fc.beforeEach: skip ← 1st run, do nothing
    run(v1)
    fc.afterEach: (noop)

    fc.beforeEach: afterEach + beforeEach ← closed the 1st run, opened the 2nd
    run(v2)
    fc.afterEach: (noop)

    fc.beforeEach: afterEach + beforeEach ← closed the 2nd run, opened the 3rd
    run(v3)
    fc.afterEach: (noop)
jest.afterEach          ← covers the last afterEach

Total: N beforeEach + N afterEach — the order is correct.

If a different approach were used, with a naive call to jest.beforeEach and jest.afterEach in fc.beforeEach and fc.afterEach, respectively, the final run would call afterEach twice:

Pass for N=3:

jest.beforeEach            ← bE #1 ✅
    fc.beforeEach: skip
    run(v1)
    fc.afterEach: jest.aE  ← aE #1 ✅

    fc.beforeEach: jest.bE ← bE #2 ✅
    run(v2)
    fc.afterEach: jest.aE  ← aE #2 ✅

    fc.beforeEach: jest.bE ← bE #3 ✅
    run(v3)
    fc.afterEach: jest.aE  ← aE #3 ✅
jest.afterEach             ← aE #4 ❌

Result: N beforeEach (ok), but N+1 afterEach (error) — jest.afterEach can't be suppressed, and it adds an extra call.

Maybe this also partially answers your question you asked here: #6586 (comment)

// - jest calls its own afterEach after the test (covers last run)
//
// Result: exactly N beforeEach + N afterEach for N property runs.
const jestState = getJestCircusState();

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.

Only working with Jest circus in CJS mode, ESM execution of Jest might not work

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.

Why do you think it won't work in ESM? jest-circus is included by default in Jest 27+ and runs independently of the CJS/ESM environment. globalThis and Symbol('JEST_STATE_SYMBOL') will also be available

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 Outdated
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 Outdated
Comment thread packages/vitest/src/internals/TestWithPropRunnerBuilder.ts Outdated
// Result: exactly N beforeEach + N afterEach for N property runs.
const test = getCurrentTest();
const suite = test?.suite || test?.file;
if (suite) {

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.

What would it mean to have no suite?

We could probably drop one level of if nesting by doing:
const beforeEachHooks = suite!==undefined?collectBeforeEachHooks(suite):[];
// same for after

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.

test is undefined if called outside a test callback (for example, in a worker thread where the _test vitest state is not set).

suite is undefined if the test has neither a parent test suite nor a file reference (a top-level test or a detached test).

I'll add a comment to the code that describes this behavior.

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.

Also, we can't do suite !== undefined ? collectBeforeEachHooks(suite) : [], because both test and suite are required to call the hooks. We must ensure that test and suite are defined before running the hooks to ensure type safety.

@rushelex rushelex changed the title 🪝 Support for beforeEach/afterEach in @fast-check/jest and @fast-check/vitest 🪝 Support for beforeEach/afterEach in @fast-check/jest Feb 10, 2026
@rushelex
rushelex requested a review from dubzzz February 11, 2026 14:30
@dubzzz

dubzzz commented Mar 8, 2026

Copy link
Copy Markdown
Owner

@rushelex now that we merged vitest one, we can come back to this one. I believe we should stick with the same philosophy as the pr we merged for vitest. So probably worth checking and updating code and test in the same spirit

@rushelex

Copy link
Copy Markdown
Contributor Author

@dubzzz Hi. Unfortunately, I don't have much time to work on this PR right now. I'd appreciate it if you could complete it, or we can wait until I have more time.

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