From 0b0106d5bd3fcedb1ea74ca7983cd6130e3a9114 Mon Sep 17 00:00:00 2001 From: Dave Wyatt Date: Sun, 17 Aug 2014 22:38:30 -0400 Subject: [PATCH] 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