Skip to content

Add Run.BeforeInvoke bootstrap for Invoke-Pester - #2772

Closed
nohwnd wants to merge 1 commit into
mainfrom
nohwnd-pester-before-invoke
Closed

Add Run.BeforeInvoke bootstrap for Invoke-Pester#2772
nohwnd wants to merge 1 commit into
mainfrom
nohwnd-pester-before-invoke

Conversation

@nohwnd

@nohwnd nohwnd commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

Adds a Run.BeforeInvoke option that runs optional bootstrap code in the caller's scope, as soon as Invoke-Pester starts — before the caller's $PesterPreference is read and before discovery. Use it to import dependencies and to provide configuration by defining or modifying $PesterPreference, which Pester then picks up as the caller preference.

This mirrors the container-level BeforeContainer convention, but for the top-level invocation itself.

How it works

Two sources, in priority order:

  1. Run.BeforeInvoke config option (scriptblock[]). When set, it wins and runs as-is.
  2. Convention file — the first Pester.BeforeInvoke.ps1 found when walking up from each Run.Path toward Run.RepoRoot is dot-sourced (deduped, in order, never escaping the repo root).

Each scriptblock is bound to the caller's SessionState and dot-sourced, so imported modules, defined functions, and $PesterPreference land in the caller's scope where Invoke-Pester reads them next. The bootstrap runs for top-level runs only, so nested Pester-in-Pester does not re-run it.

Changes

  • C#BeforeInvoke ScriptBlockArrayOption on RunConfiguration (mirrors RepoRoot/ScriptBlock).
  • src/functions/Pester.BeforeInvoke.ps1Resolve-PesterBeforeInvoke + Invoke-PesterBeforeInvoke.
  • Main.ps1 — resolves a preliminary preference and runs the bootstrap before the caller-preference read.
  • Help — generated BeforeInvoke entry in about_PesterConfiguration.
  • Teststst/Pester.BeforeInvoke.ts.ps1 (9 P-tests). Full suite green.

Open design question (reason this is a draft)

BeforeInvoke currently runs once, in the orchestrating Invoke-Pester call. For a parallel run, test containers execute in separate runspaces that would not have re-run the bootstrap — so dependency setup done here would be missing in the workers. Configuration ($PesterPreference) is inherently a once-per-invocation concern and is fine, but dependency provisioning may need to also happen per-container (e.g. via BeforeContainer) for parallel scenarios. Want to settle this before merging.

true

Introduce a Run.BeforeInvoke option that runs optional bootstrap code in
the caller's scope as soon as Invoke-Pester starts, before the caller's
$PesterPreference is read and before discovery. Use it to import
dependencies and to provide configuration by defining or modifying
$PesterPreference, which Pester then picks up as the caller preference.

Two sources, mirroring the container-level convention:

- Run.BeforeInvoke config option (scriptblock[]). When set, it wins.
- Convention file: the first Pester.BeforeInvoke.ps1 found when walking up
  from each Run.Path towards Run.RepoRoot is dot-sourced (deduped, in
  order, never escaping the repo root).

Each scriptblock is bound to the caller's SessionState and dot-sourced so
imports, defined functions and $PesterPreference land in the caller's
scope. Bootstrap runs for top-level runs only, so nested Pester-in-Pester
does not re-run it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nohwnd
nohwnd marked this pull request as draft June 27, 2026 19:26
@johlju

johlju commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

I really like this, looking forward to this merging. I will reduce a lot of duplicated code in test files by having a Pester.BeforeInvoke.ps1 that can contain bootstrap code. We can put one with different content in folders qa, unit and integration.

If it would go up to the repo root and find all and run them in sequence we could even reduce even more duplication, for example

reporoot
  |
  | -- tests    <--- finds Pester.BeforeInvoke.ps1
           |
           | -- unit
           | -- integration     <--- finds Pester.BeforeInvoke.ps1

It would then invoke them in the order top to bottom:

  1. ./tests/Pester.BeforeInvoke.ps1
  2. ./tests/integration/Pester.BeforeInvoke.ps1

But just having the option of one Pester.BeforeInvoke.ps1 would probably reduce the duplication with ~1000 rows in a larger project (like in SqlServerDsc).

@nohwnd

nohwnd commented Jun 28, 2026

Copy link
Copy Markdown
Member Author

@johlju -rc1 is out, it has similar feature, BeforeContainer, my main problem is that we cannot just promise that it will run once, because if you need to run tests in parallel (there is experimental parallel mode), this script needs to run in every new runspace. And I wanted to minimize the overhead of looking the file up every time, and did not want to think about how to say "this is in child folder, but don't run any of the script files you find above me" like "root=true" in .editorconfig.

but all those are good suggestions, show me how they simplify your workflow, and we could at least add them as options, if not as defult. I just did not want to over design and over promise in the initial release.

@johlju

johlju commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

It says in the release notes för rc1:

"...single Pester.BeforeContainer.ps1 in the repository root..."
"... run before every test file is discovered and run..."

Assuming we can pass BeforeDiscovery, BeforeAll and AfterAll in this file it might get us a a fair bit. Some things do not run during Discovery and some things do not run during Run. Some code are duplicate, but we do slightly different setup for different things:

  • unit tests of private and public command
  • Integration tests of public commands
  • Unit tests for class-based DSC resource
  • Integration tests for class-based DSC resource
  • Unit tests for MOF-based DSC resource
  • Integration tests for MOF-based DSC resource

A single file would not handle different setup unless it can pass current test script file so the code Pester.BeforeContainer.ps1 can determine what is about to be tested.

But I will check how far the current functionality takes us and report the GAP. If we can pass BeforeDiscovery, BeforeAll and AfterAll in Pester.BeforeContainer.ps1 that will potentially reduce most duplicated code.
If it does not support BeforeDiscovery, BeforeAll and AfterAll as the release notes might suggest, then we'll see what we can put there, at least some of the BeforeDiscovery part could be moved... 🤔

@nohwnd

nohwnd commented Jun 28, 2026

Copy link
Copy Markdown
Member Author

Assuming we can pass BeforeDiscovery, BeforeAll and AfterAll

I actually don't know. I did not think about that, my thinking here was that it imports modules you need, adds helper functions etc. stuff that I typically do in a "test.ps1" script. But that now needs to happen in every child runspace.

All in all the idea of what I want to achieve here is not super clear even to me. My goal is also that I can run tests (e.g. a single It) from VSCode that need some setup, and not repeat the setup on top of every file.

A single file would not handle different setup unless it can pass current test script file so the code Pester.BeforeContainer.ps1 can determine what is about to be tested.

You could probably do that through the power of powershell, e.g. by looking at your pscall stack, but I get what you mean.

@johlju

johlju commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

My goal is also that I can run tests (e.g. a single It) from VSCode that need some setup, and not repeat the setup on top of every file.

That is why there are extensive bootstrap in each file in SqlServerDsc - so there our goal is aligned 🙂 also I need to use Invoke-PesterJob in VS Code (and outside) for tests if they load classes so I do not need to kill the session each time we re-run test.

@johlju

johlju commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

So after a bit of testing

  1. Pester.BeforeContainer.ps1 seems to support BeforeDiscovery, but not a top-level BeforeAll and AfterAll.
  2. I currently have in a BeforeDiscovery-block in each test file, I moved that successfully into a single block in Pester.BeforeContainer.ps1. The moved code works as BeforeDiscovery-block and also without such block. I don't need it to run for every container so that block would be better to put in a Run.BeforeInvoke.ps1 if that exist in the future.
  3. What I miss is a Pester.BeforeContainer.ps1 that can be placed in the same folder as the tests or a parent folder and support top-level BeforeAll and AfterAll, then I would be able to move duplicated code like this (this is simplified code, but as an example):
    BeforeAll {
         $script:moduleName = 'MyModule'
     
         Import-Module -Name $script:moduleName
     
         $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName
         $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName
         $PSDefaultParameterValues['Should-Invoke:ModuleName'] = $script:moduleName
         $PSDefaultParameterValues['Should-NotInvoke:ModuleName'] = $script:moduleName
    }
    
    AfterAll {
         $PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
         $PSDefaultParameterValues.Remove('Mock:ModuleName')
         $PSDefaultParameterValues.Remove('Should-Invoke:ModuleName')
         $PSDefaultParameterValues.Remove('Should-NotInvoke:ModuleName')
     
         # Unload the module being tested so that it doesn't impact any other tests.
         Get-Module -Name $script:moduleName -All | Remove-Module -Force
    }

@fflaten

fflaten commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

BeforeInvoke
I'm not sold on the BeforeInvoke story atm. I think BeforeContainer can handle most use-cases while supporting parallel and without overloading users with more options. Module imports, required due to parallel. Setup global requirements like dev server etc - run it conditionally by checking if running/exists/whatever.

Folder-scoped BeforeContainer
I like this, in addition to providing the context (metadata about the current container) in the future for more fine-grained conditional logic.

I wanted to minimize the overhead of looking the file up every time, and did not want to think about how to say "this is in child folder, but don't run any of the script files you find above me" like "root=true" in .editorconfig.

Now that BeforeContainer is always file-bound we could stop at first match. We discover all tests. Then Pester.BeforeContainer.ps1 in unique relevant test folders (+ up to reporoot). Build an array/lookup desc. sorted by depth and pick first match (if any) per file-type container. Users can dot-source any shared code using $PSScriptRoot relative paths if needed.

BeforeAll/AfterAll
I can see the value of centralized setup/teardown in the folder-scoped BeforeContainer files (or at that point Pester.PerContainer.ps1?). Would require rewriting how and when it's invoked + make sure you can't include Describe/Context blocks in it.

Something to explore, but maybe not critical? While it wouldn't remove the boilerplate, you could deduplicate 99% by dot-sourcing, couldn't you? BeforeAll { . "$PSScriptRoot\beforeall.ps1" }; AfterAll { . "$PSScriptRoot\afterall.ps1" };

@johlju

johlju commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Something to explore, but maybe not critical? While it wouldn't remove the boilerplate, you could deduplicate 99% by dot-sourcing, couldn't you? BeforeAll { . "$PSScriptRoot\beforeall.ps1" }; AfterAll { . "$PSScriptRoot\afterall.ps1" };

Yes, but having a standard it would be easier for contributors to understand where to look and what its for, regardless of repo. Dot-sourcing would probably work, but naming and location would most likely be different for each repo a contributor looks at. 🤔

I think BeforeContainer can handle most use-cases

I think so too. BeforeInvoke would be good for things that only need to run once for each Invoke-Pester call, not needing to run per container. But I would think it would not be used at all if several BeforeContainter can me concatenated from the parent directory tree.

@nohwnd nohwnd mentioned this pull request Jul 29, 2026
nohwnd added a commit that referenced this pull request Aug 25, 2026
…file (#2993)

* Apply Pester.BeforeContainer.ps1 from the repo root down to the test file

Until now only a single Pester.BeforeContainer.ps1 in Run.RepoRoot was used, so a
repository with different setup for unit and integration tests had to put both in
that one file, or repeat them in every test file. Now every
Pester.BeforeContainer.ps1 from Run.RepoRoot down to the test file's own folder is
applied, outermost first, so shared setup lives at the root and the parts only some
tests need live next to those tests. Requested by @johlju in #2772, where he
estimated it removes around a thousand lines of duplication in SqlServerDsc.

The setup files are dot-sourced into the container's own scope now, not into the
run session state, which is what makes the folders actually scope anything. Before
this, setup dot-sourced for one file stayed visible to every container after it, so
a file in tests/integration would silently inherit whatever tests/unit had set up
and the result depended on run order.

A block can hold more than one of each setup and teardown as a result. The folder
setup and the test file's own BeforeAll both register on the container's root block
and have to compose instead of one of them erroring out:

- Setup and teardown on Block are Pester.ScriptBlockCollection, a List<ScriptBlock>
  that renders as its contents, so a single one prints exactly what a plain
  ScriptBlock printed before and several print with the [n] numbering used for
  multiple errors.
- Setups run in the order they were registered, teardowns in reverse.
- A folder can opt out of everything above it with #pester:no-inherit, matched on
  real comment tokens like #pester:no-parallel. Same meaning as root = true in an
  .editorconfig. Useful for a folder like doc tests that needs its own cheap setup
  and should not pay for the expensive one.

Parallel resolves the chain once in the parent and hands each worker the paths.
Workers are separate runspaces that cannot share a cache, so letting them resolve
would mean rediscovering the same folders over and over, in parallel.

Breaking changes:

- Two BeforeAll (or AfterAll, BeforeEach, AfterEach) in one block no longer throw.
- Top level code in Pester.BeforeContainer.ps1 runs during discovery only, so it has
  to be in BeforeAll to reach the tests. Same rule a test file already follows, and
  it is what keeps a folder's setup out of the next container.
- Stray output from a setup file no longer warns, it cannot escape the container to
  reach Invoke-Test anymore. Split-RSpecResult still covers the filtering itself.

Note: resolving during the Find-FileInDirectory walk instead of walking up per
folder, and reporting the applied setup files on the container, are still open.

🤖

* Fix braces lost when resolving the merge in the parallel tests

🤖

* Cache the setup file chain per directory, not per container folder

Which Pester.BeforeContainer.ps1 files apply is a property of the directory, so
the cache belongs on the directory. It used to be keyed by the container's own
folder, which meant every distinct test folder walked and tokenized the whole
chain above it again. On a tree with 60 test folders and a 26 KB root setup file
that is 845 ms of resolving, and all of it sits in front of the run, before any
parallel worker starts.

Get-PesterBeforeContainerChain now takes the cache, walks up only as far as the
first directory that is already resolved, and records the list for every
directory it passes on the way back down. Each directory is checked on disk once
per run and each setup file is tokenized once per run. The same tree resolves in
23 ms.

#pester:no-inherit does not need anything special. The opt-out belongs to the
folder that carries it, so the truncated list is simply what gets cached for that
folder, and folders below inherit the shorter list without looking above it
again.

Added tests for the cache entry per directory, for the walk stopping at an entry
that is already there, and for the truncated chain being what gets cached under
a no-inherit folder.

🤖
@nohwnd

nohwnd commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Superseded by #2993 thanks for the input @johlju .

@nohwnd nohwnd closed this Aug 29, 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.

3 participants