Pretend I have a function like...
function Get-Something {
return Get-DogShit
}
...in my Pester test script...
$var = 1
Mock 'Get-Dogshit' { return $var }
it 'should return true' {
Get-Something | should $var
}
This doesn't work, but you see what I'm trying to do here? I want to get the value from a local variable into a MOCK script block. I want to avoid hard coding the return value in the mock and the expected result in the it-block. Any ideas on how I can achieve this?
I had this problem myself, and script scope didn't work and I didn't care to use global scope. A bit of research shows how you can use closures for this.
$var = 1
Mock 'Get-Dogshit' { return $var }.GetNewClosure()
it 'should return true' {
Get-Something | Should be $var
}
Wasn't sure if it would work as not messed with pester before but apparently it follows the same scope rules as standard PowerShell.
So $script:var = 1, and the brute-force $global:var = 1 if it doesn't or if you need to call it from outside the script scope.
Related
In Single Module Scenario: Running Set-Var returns 10.
# m.psm1
function Set-Var {
$MyVar = 10
Get-Var
}
function Get-Var {
$MyVar
}
In Nested Modules Scenario: Running Set-Var does not return any value.
# m1.psm1
function Get-Var {
$MyVar
}
# m.psm1
Import-Module .\m1.psm1
function Set-Var {
$MyVar = 10
Get-Var
}
How do I achieve the same effect as a single module with nested modules? Using $script:MyVar also does not work. However, I would like to keep the scope of the variable local to enable concurrent executions with different values.
Your code doesn't work because local variables are not inherited by functions in nested module context.
You can do this instead:
$null = New-Module {
function Get-Var {
[CmdletBinding()] param()
$PSCmdlet.SessionState.PSVariable.Get('MyVar').Value
}
}
The New-Module command creates an in-memory module, because this code only works when the caller is in a different module or script.
Use the CmdletBinding attribute to create an advanced function. This is a prerequisite to use the automatic $PSCmdlet variable, which we need in the next step.
Use its SessionState.PSVariable member to get or set a variable from the parent (module) scope.
This answer shows an example how to set a variable in the parent (module) scope.
See also: Is there any way for a powershell module to get at its caller's scope?
I setup two Mocks in two different It '' blocks for a IIS Method Get-IISSite...
The problem I ran into is those two it's pass, but when I try to do a regular test that doesn't use the mock (its a new 'it' in the same 'context) the method returns the mocks even in separate Its. I read some documentation on another page suggesting that Mocks are available to parent (same context)
This concerns me a bit, because I was hoping to find a mechanism to dispose of the mock once its no longer needed inside the It to restore it. Is it necessary to have non mocks of the same function in a separate describe and or context? (It certainly appears that way)
Code as follows:
Wrote a method to wrap around the empty call to Get-IISSite
### Get all IIS website collection
function IIS-SiteGetAll() {
$sites = Get-IISSite
if ($sites.Length -eq 0 -or $sites -eq $null) {
throw 'No IIS Sites found, please check that IIS is installed and service is running.'
}
return $sites
}
Snippet of the Test
Describe 'IIS Site Methods' {
Context 'IIS-SiteGetAll (mocked)' {
It 'Throws error if number of sites returned is 0' {
# Get-IISSite is the Powershell ISS command, we want it to return an empty collection
Mock -CommandName Get-IISSite {
return #()
}
{ IIS-SiteGetAll } | Should Throw
}
It 'Throws error if number of sites returned is $null' {
# Get-IISSite is the Powershell ISS command, we want it to return an empty collection
Mock -CommandName Get-IISSite {
return $null
}
{ IIS-SiteGetAll } | Should Throw
}
It 'Returns collection of sites' { #IIS-SiteGetAll fails because it points to one of the two Mocks in the It above
$actual = IIS-SiteGetAll
$actual | Should -BeOfType [Microsoft.Web.Administration.Site]
$actual.Length | Should -BeGreaterThan 0
}
}
Context 'IIS-SiteGetAll' {
It 'Returns collection of sites' { # This separate chain exact same test passes.
$actual = IIS-SiteGetAll
$actual | Should BeOfType [Microsoft.Web.Administration.Site]
$actual.Length | Should -BeGreaterThan 0
}
}
}
Note powershell is a bit fuzzy because if you do GetType on actual for the second one it shows as Object[] with BaseType of System.Array (but that fails) only the [Microsoft.Web.Administration.Site] output class seems to work which matches documentation here: https://learn.microsoft.com/en-us/powershell/module/iisadministration/get-iissite?view=win10-ps
I'm not aware of another way around this, but I had to find the answer in a blog, not in Pester's documentation.
Submitting as an answer as OP says it's what he ended up doing:
Granted I don't normally create a Mock in the It block; I'd do what you have done here and create a separate Context for it.
There is an AfterEach block you can use in Pester that will run after each It block but you'd need to know how to remove a Mock first to use that...
I want to be able to refer to $_ object in catch block from the function I call in catch block like this:
function foo
{
$something = ((Get-Variable -Name "_" -Scope 1).Value).Exception.Message
Write-Host $something
}
I want to use this in situations like these:
foo #write-host should print empty line
try {
throw
} catch {
foo #write-host should print $_.Exception.Message from this catch block
}
How to do that properly?
The goal is to avoid passing $_ as parameter to foo every time I use it in a catch block, and not to print anything when I call foo not in a catch block.
I have also tried this:
function foo
{
$something = (($ExecutionContext.SessionState.PSVariable.Get("_")).Value).Exception.Message
Write-Host $something
}
This seems to produce the result I want when working interactively, but not when launching script.
The clean way of doing this is, of course, an (optional) parameter on foo, called with foo $_ from catch blocks. This makes it perfectly clear what we're calling foo with, it has no issues with scoping, it makes foo testable, all that niceness. You will see this approach in most anything that prints errors (as in the answers to this question, for example).
Even so, if you insist on plucking the error variable from the parent frame, it can be done:
function foo {
$lastError = (Get-PSCallStack)[1].GetFrameVariables()["_"].Value
if ([Object]::ReferenceEquals($lastError.Exception, $error[0].Exception)) {
$lastError.Exception.Message
}
}
Note the extra trickery with Object.ReferenceEquals to be absolutely sure that $_ is referring to the last error record and not some arbitrary pipeline item (which also use $_). This will still fail if the error record itself is in the pipeline (e.g. $error |% { foo }), but at that point you might as well call it a feature.
Let's take the classic first-order functions example:
function Get-MyName { "George" }
function Say-Hi([scriptblock]$to) {
Write-Host ("Hi "+(& $to))
}
This works just fine:
Say-Hi { "Fred Flintstone" }
this does not:
Say-Hi Get-MyName
because Get-MyName is evaluated, not passed as a value itself. How do I pass Get-MyName as a value?
You have to pass Get-Myname as a scriptblock, because that's how you've defined the variable type.
Say-Hi ${function:Get-MyName}
If you are ready to sacrifice the [scriptblock] parameter type declaration
then there is one more way, arguably the simplest to use and effective. Just
remove [scriptblock] from the parameter (or replace it with [object]):
function Get-MyName { "George" }
function Say-Hi($to) {
Write-Host ("Hi "+(& $to))
}
Say-Hi Get-MyName
Say-Hi { "George" }
So now $to can be a script block or a command name (not just a function but also alias, cmdlet, and script).
The only disadvantage is that the declaration of Say-Hi is not so self describing.
And, of course, if you do not own the code and cannot change it then this is
not applicable at all.
I wish PowerShell has a special type for this, see this suggestion.
In that case function Say-Hi([command]$to) would be ideal.
This might be a better example to illustrate the question, and details of execution scope. #mjolinor's answer appears to work nicely for this use case:
function Get-MyName($name) { $name; throw "Meh" }
function Say-Hi([scriptblock]$to) {
try {
Write-Host ("Hi "+(& $to $args)) # pass all other args to scriptblock
} catch {
Write-Host "Well hello, $_ exception!"
}
}
The command and its output:
PS C:\> Say-Hi ${function:Get-MyName} 'George'
Well hello, Meh exception
In particular, I'm using this pattern for wrapping functions that work with a flaky remote SQL Server database connection, which sleep, then retry several times before finally succeeding or throwing a higher exception.
Strictly based on your code, the correct answer is this:
Say-Hi {(Get-MyName)}
This will produce "Hi George"
User cashfoley has posted what appears to be a fairly elegant set of code at codeplex for a "module" called PSClass.
When I dot-source the psclass code into some code of my own, I am able to write code like:
$Animal = New-PSClass Animal {
constructor {
param( $name, $legs )
# ...
}
method -override ToString {
"A $($this.Class.ClassName) named $($this.name) with $($this.Legs) Legs"
}
}
When I tried to create a module out of the PSClass code, however, I started getting errors. The constructor and method names are no longer recognized.
Looking at the actual implementation, what I see is that constructor, method, etc. are actually nested functions inside the New-PSClass function.
Thus, it seems to me that when I dot-source the PSClass.ps1 file, my script-blocks are allowed to contain references to functions nested inside other local functions. But when the PSClass code becomes a module, with the New-PSClass function exported (I tried both using a manifest and using Export-ModuleMember), the names are no longer visible.
Can someone explain to me how the script blocks, scoping rules, and visibility rules for nested functions work together?
Also, kind of separately, is there a better class definition protocol for pure Powershell scripting? (Specifically, one that does not involve "just write it in C# and then do this...")
The variables in your script blocks don't get evaluated until they are executed. If the variables in the script block don't exist in the current scope when the block is executed, the variables won't have any values. Script blocks aren't closures: they don't capture the context at instantiation time.
Remove-variable FooBar
function New-ScriptBlock
{
$FooBar = 1
$scriptBlock = {
Write-Host "FooBar: $FooBar"
}
$FooBar = 2
& $scriptBlock # Outputs FooBar: 2 because $FooBar was set to 2 before invocation
return $scriptBlock
}
function Invoke-ScriptBlock
{
param(
$ScriptBlock
)
& $ScriptBlock
}
$scriptBlock = New-ScriptBlock
& $scriptBlock # Prints nothing since $FooBar doesn't exist in this scope
$FooBar = 3
Invoke-ScriptBlock $scriptBlock # Prints $FooBar: 3 since FooBar set to 3