From 32c8019314b604ebe13afe864bc2e9d23dac371b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sun, 19 Jul 2026 10:08:13 +0200 Subject: [PATCH] Add -Context and -Describe to BeforeEach and AfterEach (#1219) BeforeEach and AfterEach so far only run around each test. This exposes the block level variants that were scaffolded in the runtime but never had a public setter, so you can run a setup or teardown around each Context or Describe. Describe 'd' { BeforeEach -Context { <# runs before every Context in here #> } AfterEach -Context { <# runs after every Context in here #> } } No switch keeps the classic per-test behavior, and the switches combine, so BeforeEach -Context -It runs before the Context and before each test in it. -Describe and -Context inherit top down like the other setups (a setup on an outer block runs before every matching nested block), teardowns run in the opposite order. The scriptblock is stored on the block as EachContextSetup / EachDescribeSetup, and Invoke-Test walks up the ancestors and picks the one that matches how the child block was defined (FrameworkData.CommandUsed). It runs on the block's existing OuterSetup/OuterTeardown, so no extra scope is introduced. Dropped the OneTimeBlock* leftovers while here, BeforeAll -Context makes no sense since BeforeAll already runs once and its side effects persist for the whole subtree. Co-Authored-By: Claude Opus 4.8 --- src/Pester.Runtime.ps1 | 76 +++++++++++++++++++------ src/csharp/Pester/Block.cs | 8 +-- src/functions/SetupTeardown.ps1 | 42 ++++++++++++-- tst/Pester.Runtime.ts.ps1 | 45 +-------------- tst/functions/SetupTeardown.Tests.ps1 | 81 +++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 68 deletions(-) diff --git a/src/Pester.Runtime.ps1 b/src/Pester.Runtime.ps1 index 3d978d4a9..90ea4c10a 100644 --- a/src/Pester.Runtime.ps1 +++ b/src/Pester.Runtime.ps1 @@ -26,10 +26,10 @@ else { # 'New-EachTestTeardown' # 'New-OneTimeTestSetup' # 'New-OneTimeTestTeardown' -# 'New-EachBlockSetup' -# 'New-EachBlockTeardown' -# 'New-OneTimeBlockSetup' -# 'New-OneTimeBlockTeardown' +# 'New-EachDescribeSetup' +# 'New-EachContextSetup' +# 'New-EachDescribeTeardown' +# 'New-EachContextTeardown' # 'Invoke-Test', # 'Find-Test', # 'Invoke-PluginStep' @@ -390,7 +390,20 @@ function Invoke-Block ($previousBlock) { -ScriptBlock $sb ` -OuterSetup @( $(if (-not (Is-Discovery) -and (-not $Block.Skip)) { - @($previousBlock.EachBlockSetup) + @($block.OneTimeTestSetup) + # BeforeEach -Describe / -Context: walk up the ancestors and collect the + # each-block setup that matches how this block was defined, so a setup on + # an outer block runs before every matching nested block. + $__b = $previousBlock + $__eachBlockSetups = while ($null -ne $__b) { + if ('Describe' -eq $block.FrameworkData.CommandUsed) { $__b.EachDescribeSetup } + elseif ('Context' -eq $block.FrameworkData.CommandUsed) { $__b.EachContextSetup } + $__b = $__b.Parent + } + if ($null -ne $__eachBlockSetups -and 1 -lt @($__eachBlockSetups).Count) { + # collected inner-first, reverse so the outermost block runs first + [Array]::Reverse($__eachBlockSetups) + } + @($__eachBlockSetups) + @($block.OneTimeTestSetup) }) $(if (-not $Block.IsRoot) { # expand block name by evaluating the <> templates, only match templates that have at least 1 character and are not escaped by ` @@ -427,7 +440,15 @@ function Invoke-Block ($previousBlock) { }) ) ` -OuterTeardown $( if (-not (Is-Discovery) -and (-not $Block.Skip)) { - @($block.OneTimeTestTeardown) + @($previousBlock.EachBlockTeardown) + # AfterEach -Describe / -Context: mirror of the setup walk above, run inner-first + # (opposite order of the setups) after the block's own AfterAll. + $__b = $previousBlock + $__eachBlockTeardowns = while ($null -ne $__b) { + if ('Describe' -eq $block.FrameworkData.CommandUsed) { $__b.EachDescribeTeardown } + elseif ('Context' -eq $block.FrameworkData.CommandUsed) { $__b.EachContextTeardown } + $__b = $__b.Parent + } + @($block.OneTimeTestTeardown) + @($__eachBlockTeardowns) } ) ` -Context $context ` -MoveBetweenScopes ` @@ -842,38 +863,59 @@ function New-OneTimeTestTeardown { } } -# endpoint for adding a setup for each block in the current block -function New-EachBlockSetup { +# endpoint for adding a setup for each Describe block nested in the current block +function New-EachDescribeSetup { param ( [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock ) if (Is-Discovery) { - $state.CurrentBlock.EachBlockSetup = $ScriptBlock + if ($null -ne $state.CurrentBlock.EachDescribeSetup) { + throw "BeforeEach -Describe is already defined in this block. Each block can only have one BeforeEach -Describe. Combine the code into a single block." + } + $state.CurrentBlock.EachDescribeSetup = $ScriptBlock } } -# endpoint for adding a teardown for each block in the current block -function New-EachBlockTeardown { +# endpoint for adding a setup for each Context block nested in the current block +function New-EachContextSetup { param ( [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock ) if (Is-Discovery) { - $state.CurrentBlock.EachBlockTeardown = $ScriptBlock + if ($null -ne $state.CurrentBlock.EachContextSetup) { + throw "BeforeEach -Context is already defined in this block. Each block can only have one BeforeEach -Context. Combine the code into a single block." + } + $state.CurrentBlock.EachContextSetup = $ScriptBlock } } -# endpoint for adding a setup for all blocks in the current block -# endpoint for adding a teardown for all clocks in the current block -function New-OneTimeBlockTeardown { - [CmdletBinding()] +# endpoint for adding a teardown for each Describe block nested in the current block +function New-EachDescribeTeardown { + param ( + [Parameter(Mandatory = $true)] + [ScriptBlock] $ScriptBlock + ) + if (Is-Discovery) { + if ($null -ne $state.CurrentBlock.EachDescribeTeardown) { + throw "AfterEach -Describe is already defined in this block. Each block can only have one AfterEach -Describe. Combine the code into a single block." + } + $state.CurrentBlock.EachDescribeTeardown = $ScriptBlock + } +} + +# endpoint for adding a teardown for each Context block nested in the current block +function New-EachContextTeardown { param ( [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock ) if (Is-Discovery) { - $state.CurrentBlock.OneTimeBlockTeardown = $ScriptBlock + if ($null -ne $state.CurrentBlock.EachContextTeardown) { + throw "AfterEach -Context is already defined in this block. Each block can only have one AfterEach -Context. Combine the code into a single block." + } + $state.CurrentBlock.EachContextTeardown = $ScriptBlock } } diff --git a/src/csharp/Pester/Block.cs b/src/csharp/Pester/Block.cs index 1afa2c108..04030af1f 100644 --- a/src/csharp/Pester/Block.cs +++ b/src/csharp/Pester/Block.cs @@ -56,10 +56,10 @@ public Block() public ScriptBlock OneTimeTestSetup { get; set; } public ScriptBlock EachTestTeardown { get; set; } public ScriptBlock OneTimeTestTeardown { get; set; } - public ScriptBlock EachBlockSetup { get; set; } - public ScriptBlock OneTimeBlockSetup { get; set; } - public ScriptBlock EachBlockTeardown { get; set; } - public ScriptBlock OneTimeBlockTeardown { get; set; } + public ScriptBlock EachDescribeSetup { get; set; } + public ScriptBlock EachContextSetup { get; set; } + public ScriptBlock EachDescribeTeardown { get; set; } + public ScriptBlock EachContextTeardown { get; set; } public List Order { get; set; } = new List(); public bool Passed { get; set; } diff --git a/src/functions/SetupTeardown.ps1 b/src/functions/SetupTeardown.ps1 index b83c61a24..40250a69e 100644 --- a/src/functions/SetupTeardown.ps1 +++ b/src/functions/SetupTeardown.ps1 @@ -54,12 +54,29 @@ [Parameter(Mandatory = $true, Position = 1)] [Scriptblock] - $Scriptblock + $Scriptblock, + + # Run before each test (It) in the current block and its descendants. This is the default + # when no target switch is provided. + [switch] $It, + + # Run before each Describe block nested in the current block. + [switch] $Describe, + + # Run before each Context block nested in the current block. + [switch] $Context ) Assert-DescribeInProgress -CommandName BeforeEach Assert-BoundScriptBlockInput -ScriptBlock $Scriptblock - New-EachTestSetup -ScriptBlock $Scriptblock + # keep the classic per-test behavior when no target switch is given + if (-not ($It -or $Describe -or $Context)) { + $It = $true + } + + if ($It) { New-EachTestSetup -ScriptBlock $Scriptblock } + if ($Describe) { New-EachDescribeSetup -ScriptBlock $Scriptblock } + if ($Context) { New-EachContextSetup -ScriptBlock $Scriptblock } } function AfterEach { @@ -121,12 +138,29 @@ function AfterEach { [Parameter(Mandatory = $true, Position = 1)] [Scriptblock] - $Scriptblock + $Scriptblock, + + # Run after each test (It) in the current block and its descendants. This is the default + # when no target switch is provided. + [switch] $It, + + # Run after each Describe block nested in the current block. + [switch] $Describe, + + # Run after each Context block nested in the current block. + [switch] $Context ) Assert-DescribeInProgress -CommandName AfterEach Assert-BoundScriptBlockInput -ScriptBlock $Scriptblock - New-EachTestTeardown -ScriptBlock $Scriptblock + # keep the classic per-test behavior when no target switch is given + if (-not ($It -or $Describe -or $Context)) { + $It = $true + } + + if ($It) { New-EachTestTeardown -ScriptBlock $Scriptblock } + if ($Describe) { New-EachDescribeTeardown -ScriptBlock $Scriptblock } + if ($Context) { New-EachContextTeardown -ScriptBlock $Scriptblock } } function BeforeAll { diff --git a/tst/Pester.Runtime.ts.ps1 b/tst/Pester.Runtime.ts.ps1 index a6d21bd99..b7615bc13 100644 --- a/tst/Pester.Runtime.ts.ps1 +++ b/tst/Pester.Runtime.ts.ps1 @@ -1296,47 +1296,6 @@ i -PassThru:$PassThru { $container.OneTimeTestTeardown | Verify-Equal 1 } - t "block setups&teardowns run only when there are some tests to run in the block" { - $container = [PSCustomObject]@{ - OneTimeBlockSetup1 = 0 - EachBlockSetup1 = 0 - EachBlockTeardown1 = 0 - OneTimeBlockTeardown1 = 0 - } - $actual = Invoke-Test -SessionState $ExecutionContext.SessionState -BlockContainer (New-BlockContainerObject -ScriptBlock { - - # New-OneTimeBlockSetup { $container.OneTimeBlockSetup1++} - New-EachBlockSetup { - $container.EachBlockSetup1++ - } - - New-Block 'block1' { - New-Test "test1" { - "here" - } - } - - New-Block 'no test block' { - - } - - New-Block 'no running tests' { - New-Test "do not run test" -Tag "DoNotRun" { - } - } - - New-EachBlockTeardown { - $container.EachBlockTeardown1++ - } - # New-OneTimeBlockTeardown { $container.OneTimeBlockTeardown1++ } - }) -Filter (New-FilterObject -ExcludeTag DoNotRun) - - # $container.OneTimeBlockSetup1 | Verify-Equal 1 - $container.EachBlockSetup1 | Verify-Equal 1 - $container.EachBlockTeardown1 | Verify-Equal 1 - # $container.OneTimeBlockTeardown1 | Verify-Equal 1 - } - t 'setup and teardown are executed on skipped parent blocks when a test is explicitly included' { $container = @{ OneTimeTestSetup = 0 @@ -2262,7 +2221,7 @@ i -PassThru:$PassThru { $Value | Verify-Equal 1 } - New-OneTimeBlockTeardown { + New-OneTimeTestTeardown { $Value | Verify-Equal 1 } @@ -2289,7 +2248,7 @@ i -PassThru:$PassThru { $Color = "Blue" } - New-OneTimeBlockTeardown { + New-OneTimeTestTeardown { $Value | Verify-Equal 1 } diff --git a/tst/functions/SetupTeardown.Tests.ps1 b/tst/functions/SetupTeardown.Tests.ps1 index 71d0347be..f15bb14dd 100644 --- a/tst/functions/SetupTeardown.Tests.ps1 +++ b/tst/functions/SetupTeardown.Tests.ps1 @@ -310,3 +310,84 @@ Describe 'Duplicate setup and teardown blocks throw' { #} #Testing if failing setup or teardown will fail 'It' is done in the Pester.Runtime.ts.ps1 file ("failing one time block test setups and teardowns") + +Describe 'BeforeEach/AfterEach -Context and -Describe (#1219)' { + BeforeAll { + $blockScoped = @{ + Order = [System.Collections.Generic.List[string]]::new() + InheritedSetup = 0 + DescribeSetup = 0 + Combined = [System.Collections.Generic.List[string]]::new() + } + } + + Describe 'a -Context setup and teardown wrap each Context' { + BeforeEach -Context { $blockScoped.Order.Add('setup') } + AfterEach -Context { $blockScoped.Order.Add('teardown') } + + Context 'A' { + It 'a' { $blockScoped.Order.Add('A') } + } + + Context 'B' { + It 'b' { $blockScoped.Order.Add('B') } + } + } + + Describe 'the recorded -Context order' { + It 'ran the setup before and the teardown after each Context, in order' { + ($blockScoped.Order -join ',') | Should -Be 'setup,A,teardown,setup,B,teardown' + } + } + + Describe 'a -Context setup does not run for a test directly in the block' { + BeforeEach -Context { $blockScoped.DirectContextSetupRan = $true } + + It 'a test that is not inside a Context is not wrapped' { + $blockScoped.ContainsKey('DirectContextSetupRan') | Should -BeFalse + } + } + + Describe 'a -Context setup is inherited by nested Contexts' { + BeforeEach -Context { $blockScoped.InheritedSetup++ } + + Context 'outer' { + Context 'inner' { + It 'ran the inherited setup entering both the outer and the inner Context' { + $blockScoped.InheritedSetup | Should -Be 2 + } + } + } + } + + Describe 'a -Describe setup targets Describe blocks, not Context blocks' { + BeforeEach -Describe { $blockScoped.DescribeSetup++ } + + Context 'a context' { + It 'did not run the -Describe setup for a Context' { + $blockScoped.DescribeSetup | Should -Be 0 + } + } + + Describe 'a nested describe' { + It 'ran the -Describe setup for a nested Describe' { + $blockScoped.DescribeSetup | Should -Be 1 + } + } + } + + Describe 'a -Context -It setup runs before the Context and before each test in it' { + BeforeEach -Context -It { $blockScoped.Combined.Add('setup') } + + Context 'c' { + It 'first' { $blockScoped.Combined.Add('first') } + It 'second' { $blockScoped.Combined.Add('second') } + } + } + + Describe 'the recorded -Context -It order' { + It 'ran once entering the Context and once before each test' { + ($blockScoped.Combined -join ',') | Should -Be 'setup,setup,first,setup,second' + } + } +}