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' + } + } +}