From 9df39610e3eb2bd92ac09871dc1a3d3a32589eba Mon Sep 17 00:00:00 2001 From: Dave Wyatt Date: Sat, 16 Aug 2014 09:58:37 -0400 Subject: [PATCH 1/3] Help Updates Beginning to update comment-based help and wiki pages to match each other (and be accurate for v3.0 usage.) --- Functions/Context.ps1 | 30 +++++++++----- Functions/Describe.ps1 | 48 ++++++++++++---------- Functions/SetupTeardown.ps1 | 30 ++++++++++++++ en-US/about_BeforeEach_AfterEach.help.txt | 49 +++++++++++++++++++++++ 4 files changed, 125 insertions(+), 32 deletions(-) create mode 100644 en-US/about_BeforeEach_AfterEach.help.txt diff --git a/Functions/Context.ps1 b/Functions/Context.ps1 index 759d1c6c6..d625bf576 100644 --- a/Functions/Context.ps1 +++ b/Functions/Context.ps1 @@ -1,13 +1,17 @@ function Context { <# .SYNOPSIS -Provides syntactic sugar for logiclly grouping It blocks within a single Describe block. +Provides logical grouping of It blocks within a single Describe block. Any Mocks defined +inside a Context are removed at the end of the Context scope, as are any files or folders +added to the TestDrive during the Context block's execution. Any BeforeEach or AfterEach +blocks defined inside a Context also only apply to tests within that Context . .PARAMETER Name The name of the Context. This is a phrase describing a set of tests within a describe. .PARAMETER Fixture -Script that is executed. This may include setup specific to the context and one or more It blocks that validate the expected outcomes. +Script that is executed. This may include setup specific to the context and one or more It +blocks that validate the expected outcomes. .EXAMPLE function Add-Numbers($a, $b) { @@ -30,27 +34,31 @@ Describe "Add-Numbers" { .LINK Describe It +BeforeEach +AfterEach +about_Mocking about_TestDrive #> -param( - [Parameter(Mandatory = $true)] - $name, + param( + [Parameter(Mandatory = $true)] + $Name , + + [ValidateNotNull()] + [ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") + ) - [ValidateNotNull()] - [ScriptBlock] $fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") -) Assert-DescribeInProgress -CommandName Context - $Pester.EnterContext($name) + $Pester.EnterContext($Name ) $TestDriveContent = Get-TestDriveChildItem $Pester.CurrentContext | Write-Context try { - Add-SetupAndTeardown -ScriptBlock $fixture - $null = & $fixture + Add-SetupAndTeardown -ScriptBlock $Fixture + $null = & $Fixture } catch { diff --git a/Functions/Describe.ps1 b/Functions/Describe.ps1 index 61ab1e0c1..fa632a3cd 100644 --- a/Functions/Describe.ps1 +++ b/Functions/Describe.ps1 @@ -1,18 +1,23 @@ function Describe { <# .SYNOPSIS -Defines the context bounds of a test. One may use this block to -encapsulate a scenario for testing - a set of conditions assumed -to be present and that should lead to various expected results -represented by the IT blocks. +Creates a logical group of tests. All Mocks and TestDrive contents +defined within a Describe block are scoped to that Describe; they +will no longer be present when the Describe block exits. A Describe +block may contain any number of Context and It blocks. .PARAMETER Name -The name of the Test. This is often an expressive phsae describing the scenario being tested. +The name of the test group. This is often an expressive phrase describing the scenario being tested. .PARAMETER Fixture The actual test script. If you are following the AAA pattern (Arrange-Act-Assert), this typically holds the arrange and act sections. The Asserts will also lie in this block but are -typically nested each in its own IT block. +typically nested each in its own It block. Assertions are typically performed by the Should +command within the It blocks. + +.PARAMETER Tags +Optional parameter containing an array of strings. When calling Invoke-Pester, it is possible to +specify a -Tag parameter which will only execute Describe blocks containing the same Tag. .EXAMPLE function Add-Numbers($a, $b) { @@ -20,44 +25,44 @@ function Add-Numbers($a, $b) { } Describe "Add-Numbers" { - It "adds positive numbers" { $sum = Add-Numbers 2 3 - $sum.should.be(5) + $sum | Should Be 5 } It "adds negative numbers" { $sum = Add-Numbers (-2) (-2) - $sum.should.be((-4)) + $sum | Should Be (-4) } It "adds one negative number to positive number" { $sum = Add-Numbers (-2) 2 - $sum.should.be(0) + $sum | Should Be 0 } It "concatenates strings if given strings" { $sum = Add-Numbers two three - $sum.should.be("twothree") + $sum | Should Be "twothree" } - } .LINK It Context +Should Invoke-Pester +about_Mocking about_TestDrive #> -param( - [Parameter(Mandatory = $true, Position = 0)] $name, - $tags=@(), + param( + [Parameter(Mandatory = $true, Position = 0)] $Name, + $Tags=@(), [Parameter(Position = 1)] [ValidateNotNull()] - [ScriptBlock] $fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") -) + [ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") + ) if ($null -eq (Get-Variable -Name Pester -ValueOnly -ErrorAction SilentlyContinue)) { @@ -73,16 +78,17 @@ param( } #TODO add test to test tags functionality - if($pester.TagFilter -and @(Compare-Object $tags $pester.TagFilter -IncludeEqual -ExcludeDifferent).count -eq 0) {return} + if($Pester.TagFilter -and @(Compare-Object $Tags $Pester.TagFilter -IncludeEqual -ExcludeDifferent).count -eq 0) {return} $Pester.EnterDescribe($Name) + $Pester.CurrentDescribe | Write-Describe New-TestDrive try { - Add-SetupAndTeardown -ScriptBlock $fixture - $null = & $fixture + Add-SetupAndTeardown -ScriptBlock $Fixture + $null = & $Fixture } catch { @@ -100,7 +106,7 @@ param( function Assert-DescribeInProgress { param ($CommandName) - if ($null -eq $pester -or [string]::IsNullOrEmpty($pester.CurrentDescribe)) + if ($null -eq $Pester -or [string]::IsNullOrEmpty($Pester.CurrentDescribe)) { throw "The $CommandName command may only be used inside a Describe block." } diff --git a/Functions/SetupTeardown.ps1 b/Functions/SetupTeardown.ps1 index 4559679b6..d9421da89 100644 --- a/Functions/SetupTeardown.ps1 +++ b/Functions/SetupTeardown.ps1 @@ -1,10 +1,40 @@ function BeforeEach { +<# +.SYNOPSIS + Defines a series of steps to perform at the beginning of every It block within + the current Context or Describe block. + +.DESCRIPTION + BeforeEach and AfterEach are unique in that they apply to the entire Context + or Describe block, even those that come before the BeforeEach or AfterEach + definition within the Context or Describe. For a full description of this + behavior, as well as how multiple BeforeEach or AfterEach blocks interact + with each other, please refer to the about_BeforeEach_AfterEach help file. + +.LINK + about_BeforeEach_AfterEach +#> Assert-DescribeInProgress -CommandName BeforeEach } function AfterEach { +<# +.SYNOPSIS + Defines a series of steps to perform at the end of every It block within + the current Context or Describe block. + +.DESCRIPTION + BeforeEach and AfterEach are unique in that they apply to the entire Context + or Describe block, even those that come before the BeforeEach or AfterEach + definition within the Context or Describe. For a full description of this + behavior, as well as how multiple BeforeEach or AfterEach blocks interact + with each other, please refer to the about_BeforeEach_AfterEach help file. + +.LINK + about_BeforeEach_AfterEach +#> Assert-DescribeInProgress -CommandName AfterEach } diff --git a/en-US/about_BeforeEach_AfterEach.help.txt b/en-US/about_BeforeEach_AfterEach.help.txt new file mode 100644 index 000000000..5a5fa9a1d --- /dev/null +++ b/en-US/about_BeforeEach_AfterEach.help.txt @@ -0,0 +1,49 @@ +BeforeEach and AfterEach +------------------------------- + +The BeforeEach and AfterEach commands allow you to define setup and teardown tasks that are +performed at the beginning and end of every It block. This can eliminate duplication of code +in test scripts, ensure that each test is performed on a pristine state regardless of their +order, and perform any necessary cleanup tasks after each test. + +BeforeEach and AfterEach blocks may be defined inside of any Describe or Context. If they +are present in both a Context and its parent Describe, BeforeEach blocks in the Describe scope +are executed first, followed by BeforeEach blocks in the Context scope. AfterEach blocks are +the reverse of this, with the Context AfterEach blocks executing before Describe. + +The script blocks assigned to BeforeEach and AfterEach are dot-sourced in the Context or Describe +which contains the current It statement, so you don't have to worry about the scope of variable +assignments. Any variables that are assigned values within a BeforeEach block can be used inside +the body of the It block. + +Note about syntax and placement +------------------------------- + +Unlike most of the commands in a Pester script, BeforeEach and AfterEach blocks apply to the +entire Describe or Context scope in which they are defined, regardless of the order of commands +inside the Describe or Context. In other words, even if an It block appears before BeforeEach +or AfterEach in the tests file, the BeforeEach and AfterEach will still be executed. + +Examples +------------------------------- + +Describe 'Testing BeforeEach and AfterEach' { + $afterEachVariable = 'AfterEach has not been executed yet' + + It 'Demonstrates that BeforeEach may be defined after the It command' { + $beforeEachVariable | Should Be 'Set in a describe-scoped BeforeEach' + $afterEachVariable | Should Be 'AfterEach has not been executed yet' + } + + It 'Demonstrates that AfterEach has executed after the end of the first test' { + $afterEachVariable | Should Be 'AfterEach has been executed' + } + + BeforeEach { + $beforeEachVariable = 'Set in a describe-scoped BeforeEach' + } + + AfterEach { + $afterEachVariable = 'AfterEach has been executed' + } +} From ef7ed9d90f6bdd61d635bd62f14998dc65b2905c Mon Sep 17 00:00:00 2001 From: Dave Wyatt Date: Sun, 17 Aug 2014 13:11:09 -0400 Subject: [PATCH 2/3] More Help updates Also, made Name parameters to Describe / Context / It explicitly of type [string] instead of the implied [object] type. This avoids a bug which was allowing empty strings to be passed for names to these parameters, which doesn't play well with the PesterState object. --- Functions/Context.ps1 | 2 +- Functions/Describe.ps1 | 3 ++- Functions/It.ps1 | 35 ++++++++++++++++------------------- Functions/Mock.ps1 | 38 +++++++++++++++++++------------------- 4 files changed, 38 insertions(+), 40 deletions(-) diff --git a/Functions/Context.ps1 b/Functions/Context.ps1 index d625bf576..8888d635e 100644 --- a/Functions/Context.ps1 +++ b/Functions/Context.ps1 @@ -42,7 +42,7 @@ about_TestDrive #> param( [Parameter(Mandatory = $true)] - $Name , + [string] $Name, [ValidateNotNull()] [ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") diff --git a/Functions/Describe.ps1 b/Functions/Describe.ps1 index fa632a3cd..1b8637355 100644 --- a/Functions/Describe.ps1 +++ b/Functions/Describe.ps1 @@ -57,7 +57,8 @@ about_TestDrive #> param( - [Parameter(Mandatory = $true, Position = 0)] $Name, + [Parameter(Mandatory = $true, Position = 0)] + [string] $Name, $Tags=@(), [Parameter(Position = 1)] [ValidateNotNull()] diff --git a/Functions/It.ps1 b/Functions/It.ps1 index 49e727520..c3c0a4ab4 100644 --- a/Functions/It.ps1 +++ b/Functions/It.ps1 @@ -4,18 +4,16 @@ function It { Validates the results of a test inside of a Describe block. .DESCRIPTION -The It function is intended to be used inside of a Describe -Block. If you are familiar with the AAA pattern -(Arrange-Act-Assert), this would be the appropriate location -for an assert. The convention is to assert a single -expectation for each It block. The code inside of the It block -should throw an exception if the expectation of the test is not -met and thus cause the test to fail. The name of the It block -should expressively state the expectation of the test. - -In addition to using your own logic to test expectations and -throw exceptions, you may also use Pester's own helper functions -to assist in evaluating test results using the Should object. +The It command is intended to be used inside of a Describe or Context Block. +If you are familiar with the AAA pattern (Arrange-Act-Assert), the body of +the It block is the appropriate location for an assert. The convention is to +assert a single expectation for each It block. The code inside of the It block +should throw a terminating error if the expectation of the test is not met and +thus cause the test to fail. The name of the It block should expressively state +the expectation of the test. + +In addition to using your own logic to test expectations and throw exceptions, +you may also use Pester's Should command to perform assertions in plain language. .PARAMETER Name An expressive phsae describing the expected test outcome. @@ -32,27 +30,25 @@ function Add-Numbers($a, $b) { } Describe "Add-Numbers" { - It "adds positive numbers" { $sum = Add-Numbers 2 3 - $sum.should.be(5) + $sum | Should Be 5 } It "adds negative numbers" { $sum = Add-Numbers (-2) (-2) - $sum.should.be((-4)) + $sum | Should Be (-4) } It "adds one negative number to positive number" { $sum = Add-Numbers (-2) 2 - $sum.should.be(0) + $sum | Should Be 0 } It "concatenates strings if given strings" { $sum = Add-Numbers two three - $sum.should.be("twothree") + $sum | Should Be "twothree" } - } .LINK @@ -61,7 +57,8 @@ Context about_should #> param( - $name, + [Parameter(Mandatory = $true)] + [string]$name, [ScriptBlock] $test = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)") ) diff --git a/Functions/Mock.ps1 b/Functions/Mock.ps1 index a6153c2cd..c946bf652 100644 --- a/Functions/Mock.ps1 +++ b/Functions/Mock.ps1 @@ -338,16 +338,12 @@ Checks if a Mocked command has been called a certain number of times and throws an exception if it has not. .DESCRIPTION -This command checks the call history of the specified Command, in the -specified Pester scope (or any child scopes). If it had been called less -than the number of times specified (1 is the default), then an exception -is thrown. You may specify 0 times if you want to make sure that the mock -has NOT been called. If you include the Exactly switch, the number of times -that the command has been called must mach exactly with the number of -times specified on this command. +This command verifies that a mocked command has been called a certain number +of times. If the call history of the mocked command does not match the parameters +passed to Assert-MockCalled, Assert-MockCalled will throw an exception. .PARAMETER CommandName -The name of the command to check for mock calls. +The mocked command whose call history should be checked. .PARAMETER ModuleName The module where the mock being checked was injected. This is optional, @@ -358,16 +354,16 @@ The number of times that the mock must be called to avoid an exception from throwing. .PARAMETER Exactly -If this switch is present, the number specifid in Times must match -exactly the number of times the mock has been called. Otherwise it -must match "at least" the number of times specified. +If this switch is present, the number specified in Times must match +exactly the number of times the mock has been called. Otherwise it +must match "at least" the number of times specified. If the value +passed to the Times parameter is zero, the Exactly switch is implied. .PARAMETER ParameterFilter An optional filter to qualify wich calls should be counted. Only those calls to the mock whose parameters cause this filter to return true will be counted. - .PARAMETER Scope An optional parameter specifying the Pester scope in which to check for calls to the mocked command. By default, Assert-MockCalled will find @@ -391,7 +387,7 @@ C:\PS>Mock Set-Content -parameterFilter {$path.StartsWith("$env:temp\")} {... Some Code ...} -C:\PS>Assert-MockCalled Set-Content 2 {$path=$env:temp\test.txt} +C:\PS>Assert-MockCalled Set-Content 2 { $path -eq "$env:temp\test.txt" } This will throw an exception if some code calls Set-Content on $path=$env:temp\test.txt less than 2 times @@ -414,17 +410,17 @@ C:\PS>Assert-MockCalled Set-Content -Exactly 2 This will throw an exception if some code does not call Set-Content Exactly two times. .EXAMPLE -Describe 'Describe' { +Describe 'Assert-MockCalled Scope behavior' { Mock Set-Content { } - {... Some Code ...} - - It 'Calls Set-Content at least once in the Describe block' { + It 'Calls Set-Content at least once in the It block' { + {... Some Code ...} + Assert-MockCalled Set-Content -Exactly 0 -Scope It } } -Checks for calls only within the current It block +Checks for calls only within the current It block. .EXAMPLE Describe 'Describe' { @@ -443,7 +439,11 @@ and Assert-MockCalled commands use the same module name. .NOTES The parameter filter passed to Assert-MockCalled does not necessarily have to match the parameter filter (if any) which was used to create the Mock. Assert-MockCalled will find any entry in the command history -which matches its parameter filter, regardless of how the Mock was created. +which matches its parameter filter, regardless of how the Mock was created. However, if any calls to the +mocked command are made which did not match any mock's parameter filter (resulting in the original command +being executed instead of a mock), these calls to the original command are not tracked in the call history. +In other words, Assert-MockCalled can only be used to check for calls to the mocked implementation, not +to the original. #> From 0b0106d5bd3fcedb1ea74ca7983cd6130e3a9114 Mon Sep 17 00:00:00 2001 From: Dave Wyatt Date: Sun, 17 Aug 2014 22:38:30 -0400 Subject: [PATCH 3/3] More Help updates --- Functions/Context.ps1 | 1 + Functions/Describe.ps1 | 2 +- Functions/InModuleScope.ps1 | 50 +++++++++ Functions/Mock.ps1 | 126 ++++++++++------------- en-US/about_Mocking.help.txt | 180 ++++++++++++++++++++++++++------- en-US/about_TestDrive.help.txt | 2 +- en-US/about_should.help.txt | 88 ++++++++++++---- 7 files changed, 313 insertions(+), 136 deletions(-) diff --git a/Functions/Context.ps1 b/Functions/Context.ps1 index 8888d635e..a800f0933 100644 --- a/Functions/Context.ps1 +++ b/Functions/Context.ps1 @@ -36,6 +36,7 @@ Describe It BeforeEach AfterEach +about_Should about_Mocking about_TestDrive diff --git a/Functions/Describe.ps1 b/Functions/Describe.ps1 index 1b8637355..7c3ebd3c9 100644 --- a/Functions/Describe.ps1 +++ b/Functions/Describe.ps1 @@ -49,8 +49,8 @@ Describe "Add-Numbers" { .LINK It Context -Should Invoke-Pester +about_Should about_Mocking about_TestDrive diff --git a/Functions/InModuleScope.ps1 b/Functions/InModuleScope.ps1 index 8bdb50400..29b7488f8 100644 --- a/Functions/InModuleScope.ps1 +++ b/Functions/InModuleScope.ps1 @@ -1,5 +1,55 @@ function InModuleScope { +<# +.SYNOPSIS + Allows you to execute parts of a test script within the + scope of a PowerShell script module. +.DESCRIPTION + By injecting some test code into the scope of a PowerShell + script module, you can use non-exported functions, aliases + and variables inside that module, to perform unit tests on + its internal implementation. + + InModuleScope may be used anywhere inside a Pester script, + either inside or outside a Describe block. +.PARAMETER ModuleName + The name of the module into which the test code should be + injected. This module must already be loaded into the current + PowerShell session. +.PARAMETER ScriptBlock + The code to be executed within the script module. +.EXAMPLE + # The script module: + function PublicFunction + { + # Does something + } + + function PrivateFunction + { + return $true + } + + Export-ModuleMember -Function PublicFunction + + # The test script: + + Import-Module MyModule + + InModuleScope MyModule { + Describe 'Testing MyModule' { + It 'Tests the Private function' { + PrivateFunction | Should Be $true + } + } + } + + Normally you would not be able to access "PrivateFunction" from + the powershell session, because the module only exported + "PublicFunction". Using InModuleScope allowed this call to + "PrivateFunction" to work successfully. +#> + [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Functions/Mock.ps1 b/Functions/Mock.ps1 index c946bf652..b29250e2f 100644 --- a/Functions/Mock.ps1 +++ b/Functions/Mock.ps1 @@ -7,14 +7,14 @@ implementation. .DESCRIPTION This creates new behavior for any existing command within the scope of a -Describe block. The function allows you to specify a ScriptBlock that will -become the commands new behavior. +Describe or Context block. The function allows you to specify a script block +that will become the command's new behavior. -Optionally you may create a Parameter Filter which will examine the +Optionally, you may create a Parameter Filter which will examine the parameters passed to the mocked command and will invoke the mocked behavior only if the values of the parameter values pass the filter. If -they do not, the original commnd implementation will be invoked instead -of the mock. +they do not, the original command implementation will be invoked instead +of a mock. You may create multiple mocks for the same command, each using a different ParameterFilter. ParameterFilters will be evaluated in reverse order of @@ -23,17 +23,17 @@ The mock of the first filter to pass will be used. The exception to this rule are Mocks with no filters. They will always be evaluated last since they will act as a "catch all" mock. -Mocks can be marked Verifiable. If so, the Assert-VerifiableMocks can be -used to check if all Verifiable mocks were actually called. If any +Mocks can be marked Verifiable. If so, the Assert-VerifiableMocks command +can be used to check if all Verifiable mocks were actually called. If any verifiable mock is not called, Assert-VerifiableMocks will throw an exception and indicate all mocks not called. -You can mock commands on behalf of different calling scopes by using the --ModuleName parameter. If you do not specify a ModuleName, the command -is mocked in the scope of the test script. If the mocked command needs -to be called from inside a module, Mock it with the -ModuleName parameter -instead. You may mock the same command multiple times, in multiple scopes, -as necessary. +If you wish to mock commands that are called from inside a script module, +you can do so by using the -ModuleName parameter to the Mock command. This +injects the mock into the specified module. If you do not specify a +module name, the mock will be created in the same scope as the test script. +You may mock the same command multiple times, in different scopes, as needed. +Each module's mock maintains a separate call history and verified status. .PARAMETER CommandName The name of the command to be mocked. @@ -41,10 +41,14 @@ The name of the command to be mocked. .PARAMETER MockWith A ScriptBlock specifying the behvior that will be used to mock CommandName. The default is an empty ScriptBlock. +NOTE: Do not specify param or dynamicparam blocks in this script block. +These will be injected automatically based on the signature of the command +being mocked, and the MockWith script block can contain references to the +mocked commands parameter variables. .PARAMETER Verifiable -When this is set, the mock will be checked when using Assert-VerifiableMocks -to ensure the mock was called. +When this is set, the mock will be checked when Assert-VerifiableMocks is +called. .PARAMETER ParameterFilter An optional filter to limit mocking behavior only to usages of @@ -60,98 +64,62 @@ command; it doesn't necessarily have to be the same module which originally implemented the command. .EXAMPLE -Mock Get-ChildItem {return @{FullName="A_File.TXT"}} +Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } -Using this Mock, all calls to Get-ChildItem will return an object with a +Using this Mock, all calls to Get-ChildItem will return a hashtable with a FullName property returning "A_File.TXT" .EXAMPLE -Mock Get-ChildItem {return @{FullName="A_File.TXT"}} -ParameterFilter {$Path.StartsWith($env:temp)} +Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } -ParameterFilter { $Path.StartsWith($env:temp) } This Mock will only be applied to Get-ChildItem calls within the user's temp directory. .EXAMPLE -Mock Set-Content -Verifiable -ParameterFilter {$Path -eq "some_path" -and $Value -eq "Expected Value"} +Mock Set-Content {} -Verifiable -ParameterFilter { $Path -eq "some_path" -and $Value -eq "Expected Value" } When this mock is used, if the Mock is never invoked and Assert-VerifiableMocks is called, an exception will be thrown. The command behavior will do nothing since the ScriptBlock is empty. .EXAMPLE -c:\PS>Mock Get-ChildItem {return @{FullName="A_File.TXT"}} -ParameterFilter {$Path.StartsWith($env:temp\1)} -c:\PS>Mock Get-ChildItem {return @{FullName="B_File.TXT"}} -ParameterFilter {$Path.StartsWith($env:temp\2)} -c:\PS>Mock Get-ChildItem {return @{FullName="C_File.TXT"}} -ParameterFilter {$Path.StartsWith($env:temp\3)} +Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } -ParameterFilter { $Path.StartsWith($env:temp\1) } +Mock Get-ChildItem { return @{FullName = "B_File.TXT"} } -ParameterFilter { $Path.StartsWith($env:temp\2) } +Mock Get-ChildItem { return @{FullName = "C_File.TXT"} } -ParameterFilter { $Path.StartsWith($env:temp\3) } Multiple mocks of the same command may be used. The parameter filter determines which is invoked. Here, if Get-ChildItem is called on the "2" directory of the temp folder, then B_File.txt will be returned. .EXAMPLE -Mock Get-ChildItem {return @{FullName="B_File.TXT"}} -ParameterFilter {$Path -eq "$env:temp\me"} -Mock Get-ChildItem {return @{FullName="A_File.TXT"}} -ParameterFilter {$Path.StartsWith($env:temp)} +Mock Get-ChildItem { return @{FullName="B_File.TXT"} } -ParameterFilter { $Path -eq "$env:temp\me" } +Mock Get-ChildItem { return @{FullName="A_File.TXT"} } -ParameterFilter { $Path.StartsWith($env:temp) } Get-ChildItem $env:temp\me -Here, both mocks could apply since both filters will pass. A_File.TXT will be returned because it was the last Mock created. +Here, both mocks could apply since both filters will pass. A_File.TXT will be returned because it was the most recent Mock created. .EXAMPLE -Mock Get-ChildItem {return @{FullName="B_File.TXT"}} -ParameterFilter {$Path -eq "$env:temp\me"} -Mock Get-ChildItem {return @{FullName="A_File.TXT"}} +Mock Get-ChildItem { return @{FullName = "B_File.TXT"} } -ParameterFilter { $Path -eq "$env:temp\me" } +Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } Get-ChildItem c:\windows -Here, A_File.TXT will be returned. Since no filterwas specified, it will apply to any call to Get-ChildItem that does not pass another filter. +Here, A_File.TXT will be returned. Since no filter was specified, it will apply to any call to Get-ChildItem that does not pass another filter. .EXAMPLE -Mock Get-ChildItem {return @{FullName="B_File.TXT"}} -ParameterFilter {$Path -eq "$env:temp\me"} -Mock Get-ChildItem {return @{FullName="A_File.TXT"}} +Mock Get-ChildItem { return @{FullName = "B_File.TXT"} } -ParameterFilter { $Path -eq "$env:temp\me" } +Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } Get-ChildItem $env:temp\me -Here, B_File.TXT will be returned. Even though the filterless mock was created last. This illustrates that filterless Mocks are always evaluated last regardlss of their creation order. +Here, B_File.TXT will be returned. Even though the filterless mock was created more recently. This illustrates that filterless Mocks are always evaluated last regardlss of their creation order. .EXAMPLE -Mock -ModuleName MyTestModule Get-ChildItem {return @{FullName="A_File.TXT"}} +Mock Get-ChildItem { return @{FullName = "A_File.TXT"} } -ModuleName MyTestModule Using this Mock, all calls to Get-ChildItem from within the MyTestModule module -will return an object with a FullName property returning "A_File.TXT" +will return a hashtable with a FullName property returning "A_File.TXT" .EXAMPLE - Describe "BuildIfChanged" { - Mock Get-Version {return 1.1} - Context "Wnen there are Changes" { - Mock Get-NextVersion {return 1.2} - Mock Build {} -Verifiable -ParameterFilter {$version -eq 1.2} - - $result = BuildIfChanged - - It "Builds the next version" { - Assert-VerifiableMocks - } - It "returns the next version number" { - $result.Should.Be(1.2) - } - } - Context "Wnen there are no Changes" { - Mock Get-NextVersion -MockWith {return 1.1} - Mock Build -MockWith {} - - $result = BuildIfChanged - - It "Should not build the next version" { - Assert-MockCalled Build -Times 0 -ParameterFilter{$version -eq 1.1} - } - } - } - - Notice how 'Mock Get-Version {return 1.1}' is declared within the - Describe block. This allows all Context and It blocks inside the describe - to use this Mock. If a context scoped mock, mocks Get-Version, that mock - will override the describe scoped mock within that contex tif both mocks - apply to the parameters passed to Get-Version. - -.EXAMPLE -Mock internal module function with Mock. - Get-Module -Name ModuleMockExample | Remove-Module New-Module -Name ModuleMockExample -ScriptBlock { - function Hidden {"Hidden"} + function Hidden { "Internal Module Function" } function Exported { Hidden } Export-ModuleMember -Function Exported @@ -160,20 +128,30 @@ New-Module -Name ModuleMockExample -ScriptBlock { Describe "ModuleMockExample" { It "Hidden function is not directly accessible outside the module" { - { ModuleMockExample\Hidden } | Should Throw + { Hidden } | Should Throw } It "Original Hidden function is called" { - Exported | Should Be "Hidden" + Exported | Should Be "Internal Module Function" } It "Hidden is replaced with our implementation" { - Mock Hidden { "mocked" } -ModuleName ModuleMockExample - Exported | Should Be "mocked" + Mock Hidden { "Mocked" } -ModuleName ModuleMockExample + Exported | Should Be "Mocked" } } +This example shows how calls to commands made from inside a module can be +mocked by using the -ModuleName parameter. + + .LINK +Assert-MockCalled +Assert-VerifiableMocks +Describe +Context +It +about_Should about_Mocking #> diff --git a/en-US/about_Mocking.help.txt b/en-US/about_Mocking.help.txt index d16af3ed1..a9c1e6cdc 100644 --- a/en-US/about_Mocking.help.txt +++ b/en-US/about_Mocking.help.txt @@ -13,7 +13,7 @@ DESCRIPTION - Mock the behavior of ANY powershell command. - Verify that specific commands were (or were not) called. - Verify the number of times a command was called with a set of specified - parameters. + parameters. MOCKING FUNCTIONS See Get-Help for any of the below functions for more detailed information. @@ -31,54 +31,158 @@ MOCKING FUNCTIONS and throws an exception if it has not. EXAMPLE - function BuildIfChanged { - $thisVersion=Get-Version - $nextVersion=Get-NextVersion - if($thisVersion -ne $nextVersion) {Build $nextVersion} - return $nextVersion - } - - $here = Split-Path -Parent $MyInvocation.MyCommand.Path - $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") - . "$here\$sut" - - Describe "BuildIfChanged" { - Mock Get-Version {return 1.1} - Context "Wnen there are Changes" { - Mock Get-NextVersion {return 1.2} - Mock Build {} -Verifiable -ParameterFilter {$version -eq 1.2} - - $result = BuildIfChanged - - It "Builds the next version" { - Assert-VerifiableMocks - } - It "returns the next version number" { - $result.Should.Be(1.2) - } + function Build ($version) { + Write-Host "a build was run for version: $version" + } + + function BuildIfChanged { + $thisVersion = Get-Version + $nextVersion = Get-NextVersion + if ($thisVersion -ne $nextVersion) { Build $nextVersion } + return $nextVersion + } + + $here = Split-Path -Parent $MyInvocation.MyCommand.Path + $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") + . "$here\$sut" + + Describe "BuildIfChanged" { + Context "When there are Changes" { + Mock Get-Version {return 1.1} + Mock Get-NextVersion {return 1.2} + Mock Build {} -Verifiable -ParameterFilter {$version -eq 1.2} + + $result = BuildIfChanged + + It "Builds the next version" { + Assert-VerifiableMocks + } + It "returns the next version number" { + $result | Should Be 1.2 + } + } + Context "When there are no Changes" { + Mock Get-Version { return 1.1 } + Mock Get-NextVersion { return 1.1 } + Mock Build {} + + $result = BuildIfChanged + + It "Should not build the next version" { + Assert-MockCalled Build -Times 0 -ParameterFilter {$version -eq 1.1} + } + } + } + +MOCKING CALLS TO COMMANDS MADE FROM INSIDE SCRIPT MODULES + +Let's say you have code like this inside a script module (.psm1 file): + + function BuildIfChanged { + $thisVersion = Get-Version + $nextVersion = Get-NextVersion + if ($thisVersion -ne $nextVersion) { Build $nextVersion } + return $nextVersion + } + + function Build ($version) { + Write-Host "a build was run for version: $version" + } + + # Actual definitions of Get-Version and Get-NextVersion are not shown here, + # since we'll just be mocking them anyway. However, the commands do need to + # exist in order to be mocked, so we'll stick dummy functions here + + function Get-Version { return 0 } + function Get-NextVersion { return 0 } + + Export-ModuleMember -Function BuildIfChanged + +You wish to write a unit test for this module which mocks the calls to Get-Version +and Get-NextVersion from the module's BuildIfChanged command. In older versions of +Pester, this was not possible. As of version 3.0, there are two ways you can perform +unit tests of PowerShell script modules. The first is to inject mocks into a module: + +For these example, we'll assume that the PSM1 file is named "MyModule.psm1", and that +it is installed on your PSModulePath. + + Import-Module MyModule + + Describe "BuildIfChanged" { + Context "When there are Changes" { + Mock -ModuleName MyModule Get-Version { return 1.1 } + Mock -ModuleName MyModule Get-NextVersion { return 1.2 } + + # Just for giggles, we'll also mock Write-Host here, to demonstrate that you can + # mock calls to commands other than functions defined within the same module. + Mock -ModuleName MyModule Write-Host {} -Verifiable -ParameterFilter { + $Object -eq 'a build was run for version: 1.2' + } + + $result = BuildIfChanged + + It "Builds the next version and calls Write-Host" { + Assert-VerifiableMocks + } + + It "returns the next version number" { + $result | Should Be 1.2 + } + } + + Context "When there are no Changes" { + Mock -ModuleName MyModule Get-Version { return 1.1 } + Mock -ModuleName MyModule Get-NextVersion { return 1.1 } + Mock -ModuleName MyModule Build { } + + $result = BuildIfChanged + + It "Should not build the next version" { + Assert-MockCalled Build -ModuleName MyModule -Times 0 -ParameterFilter { + $version -eq 1.1 } - Context "Wnen there are no Changes" { - Mock Get-NextVersion -MockWith {return 1.1} - Mock Build -MockWith {} + } + } + } + +Notice that in this example test script, all calls to Mock and Assert-MockCalled have had the +-ModuleName MyModule parameter added. This tells Pester to inject the mock into the module's scope, +which causes any calls to those commands from inside the module to execute the mock instead. + +When you write your test script this way, you can mock commands that are called by the module's +internal functions. However, your test script is still limited to accessing the public, exported +members of the module. If you wanted to write a unit test that calls Build directly, for example, +it wouldn't work using the above technique. That's where the second approach to script module testing +comes into play. With Pester 3.0's InModuleScope command, you can cause entire sections of your test +script to execute inside the targeted script module. This gives you access to non-exported members of +the module. For example: + + Import-Module MyModule + + Describe "Unit testing the module's internal Build function:" { + InModuleScope MyModule { + $testVersion = 5.0 + Mock Write-Host { } - $result = BuildIfChanged + Build $testVersion - It "Should not build the next version" { - Assert-MockCalled Build -Times 0 -ParameterFilter{$version -eq 1.1} - } + It 'Outputs the correct message' { + Assert-MockCalled Write-Host -ParameterFilter { + $Object -eq "a build was run for version: $testVersion" } + } } + } - Notice how 'Mock Get-Version {return 1.1}' is declared within the - Describe block. This allows all context blocks inside the describe to - use this Mock. If a context scoped mock, mocks Get-Version, that mock - will override the describe scoped mock within that context if both mocks - apply to the parameters passed to Get-Version. +Notice that when using InModuleScope, you no longer need to specify a -ModuleName parameter when calling +Mock or Assert-MockCalled for commands within that module. You are also able to directly call the Build +function, which the module does not export. SEE ALSO Mock Assert-VerifiableMocks Assert-MockCalled + InModuleScope Describe Context It diff --git a/en-US/about_TestDrive.help.txt b/en-US/about_TestDrive.help.txt index afc92cc48..8b2352779 100644 --- a/en-US/about_TestDrive.help.txt +++ b/en-US/about_TestDrive.help.txt @@ -10,7 +10,7 @@ DESCRIPTION file activities. It is usually desirable not to perform file activity tests that will produce side effects outside of an individual test. Pester creates a PSDrive inside the user's temporary drive that is accesible via a - names PSDrive TestDrive:. Pester will remove this drive afterthe test + names PSDrive TestDrive:. Pester will remove this drive after the test completes. You may use this drive to isolate the file operations of your test to a temporary store. diff --git a/en-US/about_should.help.txt b/en-US/about_should.help.txt index f106a4ab8..ae7097ff5 100644 --- a/en-US/about_should.help.txt +++ b/en-US/about_should.help.txt @@ -21,41 +21,84 @@ SHOULD MEMBERS Compares one object with another for equality and throws if the two objects are not the same. - C:\PS>$actual="actual value" - C:\PS>$actual | Should Be "actual value" #Nothing happens - C:\PS>$actual | Should Be "not actual value" #A Pester Failure is thrown + $actual="Actual value" + $actual | Should Be "actual value" # Test will pass + $actual | Should Be "not actual value" # Test will fail - Have_Count_Of - Intended for comparing IEnumerables for the number of elements. However, - if both objects being compared do not implement IEnumerable then the - comparison will pass since both objects will be treated as though they - have a count of 1. As of Powershell version 3, a $null object compared - to a non null object will fail. They will pass in version 2.0. + BeExactly + Compares one object with another for equality and throws if the two objects are not the same. This comparison is case sensitive. - C:\PS>$actual=@(1,2,3) - C:\PS>$actual | Should Have_Count_Of @(3,2) #Will fail + $actual="Actual value" + $actual | Should BeExactly "Actual value" # Test will pass + $actual | Should BeExactly "actual value" # Test will fail Exist Does not perform any comparison but checks if the object calling Exist is presnt in a PS Provider. The object must have valid path syntax. It essentially must pass a Test-Path call. - C:\PS>$actual=(Dir . )[0].FullName - C:\PS>Remove-Item $actual - C:\PS>$actual | Should Exist #Will fail + $actual=(Dir . )[0].FullName + Remove-Item $actual + $actual | Should Exist # Test will fail - Match - Uses a regular expression to compare two objects. + Contain + Checks to see if a file contains the specified text. This search is not case sensitive and uses regular expressions. + + Set-Content -Path TestDrive:\file.txt -Value 'I am a file.' + 'TestDrive:\file.txt' | Should Contain 'I Am' # Test will pass + 'TestDrive:\file.txt' | Should Contain '^I.*file$' # Test will pass - C:\PS>"I am a value" | Should Match "I am" #Passes - C:\PS>"I am a value" | Should Match "I am a bad person" #will fail + 'TestDrive:\file.txt' | Should Contain 'I Am Not' # Test will fail - Like + Tip: Use [regex]::Escape("pattern") to match the exact text. + + Set-Content -Path TestDrive:\file.txt -Value 'I am a file.' + 'TestDrive:\file.txt' | Should Contain 'I.am.a.file' # Test will pass + 'TestDrive:\file.txt' | Should Contain ([regex]::Escape('I.am.a.file')) # Test will fail - Performs a wildcard based comparison. + ContainExactly + Checks to see if a file contains the specified text. This search is case sensitive and uses regular expressions to match the text. - C:\PS>"I am a value" | Should Match "I am*" #Passes - C:\PS>"I am a value" | Should Match "*I am" #will fail + Set-Content -Path TestDrive:\file.txt -Value 'I am a file.' + 'TestDrive:\file.txt' | Should Contain 'I am' # Test will pass + 'TestDrive:\file.txt' | Should Contain 'I Am' # Test will fail + + Match + Uses a regular expression to compare two objects. This comparison is not case sensitive. + + "I am a value" | Should Match "I Am" # Test will pass + "I am a value" | Should Match "I am a bad person" # Test will fail + + Tip: Use [regex]::Escape("pattern") to match the exact text. + + "Greg" | Should Match ".reg" # Test will pass + "Greg" | Should Match ([regex]::Escape(".reg")) # Test will fail + + MatchExactly + Uses a regular expression to compare two objects. This comparison is case sensitive. + + "I am a value" | Should MatchExactly "I am" # Test will pass + "I am a value" | Should MatchExactly "I Am" # Test will fail + + Throw + Checks if an exception was thrown in the input ScriptBlock. + + { foo } | Should Throw # Test will pass + { $foo = 1 } | Should Throw # Test will fail + { foo } | Should Not Throw # Test will fail + { $foo = 1 } | Should Not Throw # Test will pass + + Warning: The input object must be a ScriptBlock, otherwise it is processed outside of the assertion. + + Get-Process -Name "process" -ErrorAction Stop | Should Throw # Should pass, but the exception thrown by Get-Process causes the test to fail. + + BeNullOrEmpty + Checks values for null or empty (strings). The static [String]::IsNullOrEmpty() method is used to do the comparison. + + $null | Should BeNullOrEmpty # Test will pass + $null | Should Not BeNullOrEmpty # Test will fail + @() | Should BeNullOrEmpty # Test will pass + "" | Should BeNullOrEmpty # Test will pass USING SHOULD IN A TEST @@ -75,4 +118,5 @@ USING SHOULD IN A TEST SEE ALSO Describe + Context It