Mock a function within a .ps1 script - unit-testing

I have a PowerShell .ps1 file which contains functions at the top of the script followed by different commands calling these functions. I am using Pester to unit test my script file.
How do I mock a function that is within my PowerShell .ps1 script?
I have tried mocking the function, but I get an error saying "could not find command".
I have also tried adding an empty "dummy" function in the describe block. This doesn't give me the above error, but it is not mocking the function within the script correctly.
I have two files. One to hold the tests and another that holds the functions and calls to the functions. Below are two examples:
File1.ps1
Function Backup-Directory([switch]$IsError)
{
If($IsError)
{
Write-Error "Error"
Exit 1
}
}
Backup-Directory $true
File2.Tests.ps1
$here = (Split-Path -Parent $MyInvocation.MyCommand.Path) -replace '\\test', '\main'
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
$productionFile = "$here\$sut"
Describe "File1" {
Context "When the Back-Directory outputs an error." {
# Arrange
Mock Back-Directory { }
Mock Write-Error
# Act
& $productionFile
$hasSucceeded = $?
# Assert
It "Calls Backup-Directory" {
Assert-MockCalled Backup-Directory -Exactly 1 -ParameterFilter {
$IsError -eq $true
}
}
It "Writes error message." {
Assert-MockCalled Write-Error -Exactly 1 -ParameterFilter {
$Message -eq "Error"
}
}
It "Exits with an error." {
$hasSucceeded | Should be $false
}
}
}

I don't think this is possible. At least with your current implementation. I asked this same question a while back... Pester Issue 414
BUT you could split out that inner function into another script file in the same directory allowing you to unit test and mock it. You would just have to dot source the function in your main script file to be able to use it:
Main-Function.ps1:
# Main script
function Main-Function {
# if debugging, set moduleRoot to current directory
if ($MyInvocation.MyCommand.Path) {
$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path
}else {
$moduleRoot = $PWD.Path
}
# dot source the inner function
. "$moduleRoot\Inner-Function.ps1"
Write-Output "This is the main script. Calling the inner function"
Inner-Function
Write-Output "Back in the main script"
}
Inner-Function.ps1:
function Inner-Function {
Write-Output "This is the inner function"
}
Main-Function.Tests.ps1:
$moduleRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
# Load Testing Function
. "$moduleRoot\Main-Function.ps1"
# Load Supporting Functions
. "$moduleRoot\Inner-Function.ps1"
Describe 'Main Script' {
Mock Inner-Function { return "This is a mocked function" }
It 'uses the mocked function' {
(Main-Function -match "This is a mocked function") | Should Be $true
}
}
This is a really nice approach because we can unit test the inner function and as the logic grows, adding tests to it is very easy (and can be done in isolation from the rest of the scripts/functions).
Inner-Functions.Tests.ps1:
$moduleRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
# Load Testing Function
. "$moduleRoot\Inner-Function.ps1"
Describe 'Inner Function' {
It 'outputs some text' {
Inner-Function | Should Be "This is the inner function"
}
}
There are two main points here...
Finding the dependent function location regardless of your current execution directory...
if ($MyInvocation.MyCommand.Path) {
$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path
}else {
$moduleRoot = $PWD.Path
}
AND
Dot Sourcing depending functions in the main function as well as the unit test files... . "$moduleRoot\Inner-Function.ps1"
Split-Path -Path $MyInvocation.MyCommand.Path is $null in a user session, but will NOT be $null when invoked from within an execution context. Meaning, you could be in C:\users\nhudacin and still load this script/module correctly. Otherwise you would have to always execute this script/module from within the same directory as where it's located without using $MyInvoation variable.

Related

Can't Pass Pester Test because of local variable variable error

I have a hard time creating a pester for a specific Powershell function using invoke-command and having a $using variable on a script block. It would always return an error whenever i run my test. Sample function and test below:
Function:
Function Execute-Function {
.
.
.
$Name = "Computer_Name"
$ScriptBlock = {
Import-Module "Activedirectory"
Get-Computer -Name $Using:Name
}
Invoke-Command -Session $Session -ScriptBlock $ScriptBlock
}
Test:
Describe 'Execute-Function' {
.
.
.
.
mock Import-Module {} -verifiable
mock Get-Computer {} -verifiable
mock Invoke-Command {
param($Scriptblock)
. $Scriptblock
} -verifiable
$result = Execute-Function
it 'should call all verifiable mocks'{
Assert-verifiablemocks
}
}
Error of my test would return A using variable cannot be retrieved. A using variable can be used only with Invoke-Command.... I can not understand this error even though I mocked the Get-Computer to return nothing? or do I need to change how I mock Get-Computer for my test to pass?
Thank You in Advance
I'm not sure you can emulate the $using: scope with Pester. You can, however, utilize the pre-$using:-scope way of doing things:
Invoke-Command -ScriptBlock {
Param(
[Parameter(Position = 0)]
[String] $Name
)
<# ... #>
} -ArgumentList $Name

Why Am I Seeing These Errors in My SSRS Powershell Script in One Environment but Not Another?

I have a Powershell Script I'm working on for post-migration SSRS report administration tasks.
In this particular scenario we have a DEV environment (where I've been primarily testing) which hosts a single instance of SSRS, and a Prod environment which is a scaled out deployment across 4 nodes.
I'm new to Powershell (just discovered it 2 days ago...) and the script I have is pretty simple:
Clear-Host 
$Username = "domain\myUsername"
$Password = "myPassword"
$Cred = New-Object System.Management.Automation.PSCredential -ArgumentList #($Username,(ConvertTo-SecureString -String $Password -AsPlainText -Force))
# Dev Connection String
$webServiceUrl = 'http://DEVwebServer.domain.com/reportserver/reportservice2010.asmx?WSDL'
# Prod Connection String
# $webServiceUrl = 'http://PRODwebServerNode1.domain.com/reportserver/reportservice2010.asmx?WSDL'
$rs = New-WebServiceProxy -Uri $webServiceUrl -Credential $Cred
 
$reports = $rs.ListChildren("/Some Folder Under Root", $true) | Where-Object { $_.TypeName -eq "Report" }
$type = $ssrsProxy.GetType().Namespace;
$schedDefType = "{0}.ScheduleDefinition" -f $type;
$schedDef = New-Object ($schedDefType)
$warning = #();
foreach ($report in $reports) {
$sched = $rs.GetExecutionOptions($report.Path, [ref]$schedDef);
$snapShotExists = $rs.ListItemHistory($report.Path);
if($sched -eq "Snapshot") {
Write-Host "Following report is configured to run from Snapshot:" -ForegroundColor Yellow
Write-Host ("Report Name: {0}`nReport Path: {1}`nExecution Type: {2}`n" -f $report.Name, $report.Path, $sched)
if ($snapShotExists) {
Write-Host "Does Snapshot Exist..?`n" -ForegroundColor Yellow
Write-Host "Yes!`tNumber of Snapshots: " $snapShotExists.Count -ForegroundColor Green
$snapShotExists.CreationDate
Write-Host "`n------------------------------------------------------------"
}
elseif (!$snapShotExists) {
Write-Host "Does Snapshot Exist..?`n" -ForegroundColor Yellow
Write-Host ("No!`n") -ForegroundColor Red
Write-Host "Creating Snapshot.......`n" -ForegroundColor Yellow
$rs.CreateItemHistorySnapshot($report.Path, [ref]$warning);
Write-Host "Snapshot Created!`n" -ForegroundColor Green
$snapShotExists.CreationDate
Write-Host "`n------------------------------------------------------------"
}
}
}
The purpose of the script is simply to recursively iterate over all of the reports for the given folder in the $reports variable, check to see if the execution type is set to "Snapshot", if it is check to see if a "History Snapshot" exists, and if one does not exist, create one.
When I run this in Dev it works just fine, but when I run in PROD I get the following error repeated for each $report in my foreach loop:
Any ideas on why this would work in one and not the other and how to overcome this error?
I was able to get this working on the Prod instance by making some adjustments using this answer as a guide:
By updating my call to New-WebServiceProxy to add a Class and Namespace flag, I was able to update the script in the following ways:
...
# Add Class and Namespace flags to New-WebServiceProxy call
$rs = New-WebServiceProxy -Class 'RS' -Namespace 'RS' -Uri $webServiceUrl -Credential $Cred
 
$reports = $rs.ListChildren("/Business and Technology Solutions", $true) | Where-Object { $_.TypeName -eq "Report" }
# Declare new "ScheduleDefintion object using the Class declared in the New-WebServiceProxy call
$schedDef = New-Object RS.ScheduleDefinition
$warning = #();
foreach ($report in $reports) {
# Referencing the "Item" property from the ScheduleDefinition
$execType = $rs.GetExecutionOptions($report.Path, [ref]$schedDef.Item)
...
I don't think the adding of the Class and Namespace flags on the New-WebServiceProxy call was exactly what did it, as I think it's just a cleaner way to ensure you're getting the proper Namespace from the WebService. Maybe just a little sugar.
I think the key change was making sure to the "Item" property from the schedule definition object, although I'm not sure why it was working in Dev without doing so...

How can I Mock Out-File when testing my PowerShell script with Pester?

I am trying to test my PowerShell code using Pester. I want to mock out-file for the following line:
$interactiveContent | Out-File -Append -FilePath (Join-Path $destDir $interactiveOutputFile)
But I want to give my own file path while testing.
I have tried the following:
Mock Out-File {
$destDir = 'c:\snmp_poc_powershell\'
$interactiveOutputFile = 'abc.csv'
}
but it is not working.
Here is a way to Mock Out-File such that it writes to a different location when running testing:
Describe 'Mocking out-file' {
$outFile = Get-Command Out-File
Mock Out-File {
$MockFilePath = '.\Test\Test.txt'
& $outFile -InputObject $InputObject -FilePath $MockFilePath -Append:$Append -Force:$Force -NoClobber:$NoClobber -NoNewline:$NoNewline
}
It 'Should mock out-file' {
"Test" | Out-File -FilePath Real.txt -Append | Should Be $null
}
}
This solution came from the developers of Pester after I raised this as an issue on Github. I had found that you can't directly call the cmdlet you are mocking from within the Mock, but they advised this solution where you use Get-Command to put the cmdlet in to a variable and then use & to invoke it instead of the cmdlet directly.
Per the other answer, Out-File doesn't return any value, so in the Pester test we simply test for $null as the result. You would probably also want to add subsequent (integration-style) tests that you test file had been created and had the values you expect.
So the code snippet is definitely a problem, your not returning any values so the mock will just be empty, however out-file never actually returns anything to begin with so I'm not exactly sure what output you are mocking? unless you just want it to pretend to output to a file and move to the next stage in your pipeline, which your current code does(so would just doing Mock Out-File {}.
However if you are looking to output to a different path why not just use that path when you create the variables for your test and not bother with a Mock?

How to mock a call to an exe file with Pester?

Developing a script in PowerShell, I require to call an external executable file(.exe). currently I am developing this script with a TDD approach, therefore I require to mock the called to this .exe file.
I try this :
Describe "Create-NewObject" {
Context "Create-Object" {
It "Runs" {
Mock '& "c:\temp\my.exe"' {return {$true}}
Create-Object| Should Be $true
}
}
}
I got this response:
Describing Create-NewObject
Context Create-Object
[-] Runs 574ms
CommandNotFoundException: Could not find Command & "C:\temp\my.exe"
at Validate-Command, C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1: line 801
at Mock, C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1: line 168
at <ScriptBlock>, C:\T\Create-NewObject.tests.ps1: line 13
Tests completed in 574ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0 Inconclusive: 0
Is there a way to mock this kind of calls without encapsulate them inside a function?
I found a way to mock the call to this executable files:
function Create-Object
{
$exp = '& "C:\temp\my.exe"'
Invoke-Expression -Command $exp
}
And the test with the mock should looks like:
Describe "Create-NewObject" {
Context "Create-Object" {
It "Runs" {
Mock Invoke-Expression {return {$true}} -ParameterFilter {($Command -eq '& "C:\temp\my.exe"')
Create-Object| Should Be $true
}
}
}
Yes, unfortunately, as of Pester 4.8.1:
you cannot mock external executables by their full paths (e.g, C:\Windows\System32\cmd.exe)
you can mock them by file name only (e.g., cmd), but beware that in older Pester versions the mock is only called for invocations that explicitly use the .exe extension (e.g., cmd.exe) - see this (obsolete) GitHub issue
Your own workaround is effective, but it involves Invoke-Expression, which is awkward; Invoke-Expression should generally be avoided
Here's a workaround that uses a helper function, Invoke-External, which wraps the invocation of external programs and, as a function, can itself be mocked, using a -ParameterFilter to filter by executable path:
In your code, define the Invoke-External function and then use it to make your call to c:\temp\my.exe:
# Helper function for invoking an external utility (executable).
# The raison d'être for this function is to allow
# calls to external executables via their *full paths* to be mocked in Pester.
function Invoke-External {
param(
[Parameter(Mandatory=$true)]
[string] $LiteralPath,
[Parameter(ValueFromRemainingArguments=$true)]
$PassThruArgs
)
& $LiteralPath $PassThruArgs
}
# Call c:\temp\my.exe via invoke-External
# Note that you may pass arguments to pass the executable as usual (none here):
Invoke-External c:\temp\my.exe
Then, to mock the call to c:\temp\my.exe in your Pester tests:
Mock Invoke-External -ParameterFilter { $LiteralPath -eq 'c:\temp\my.exe' } `
-MockWith { $true }
Note: If you only have one call to an external executable in your code, you can omit the -ParameterFilter argument.
I tried something this and seemed to work.
$PathToExe = 'C:\Windows\System32\wevtutil.exe'
New-Item -Path function: -Name $PathToExe -Value { ... }
Mock $PathToExe { ... }

Pester mock error PSInvalidCastException on Azure cmdlet call

I need to "Pester-test" 2 Azure cmdlets, Get-AzureNetworkSecurityGroup and Set-AzureNetworkSecurityRule from a PowerShell function inside a module, that looks like following:
$nsg = Get-AzureNetworkSecurityGroup -Name $NsgName
Set-AzureNetworkSecurityRule -Name $NsgRule.name `
-Type Outbound `
# ... other properties here ...
-NetworkSecurityGroup $nsg |
Format-List -Property Name,Location,Label
The parameters $NsgName, $NsgRule are not so important, the problem is I receive errors when mocking Set-AzureNetworkSecurityRule like:
Mock Get-AzureNetworkSecurityGroup { return [PSCustomObject] #{ Name='Any' } }
Mock Set-AzureNetworkSecurityRule
Mock Format-List
The error says:
[-] Error occurred in Describe block 100ms
PSInvalidCastException: Cannot convert the "#{Name=Any}" value of type "System.Management.Automation.PSCustomObject" to type "Microsoft.WindowsAzure.Commands.ServiceManagement.Network.NetworkSecurityGroup.Model.INetworkSecurityGroup".
ArgumentTransformationMetadataException: Cannot convert the "#{Name=Any}" value of type "System.Management.Automation.PSCustomObject" to type "Microsoft.WindowsAzure.Commands.ServiceManagement.Network.NetworkSecurityGroup.Model.INetworkSecurityGroup".
ParameterBindingArgumentTransformationException: Cannot process argument transformation on parameter 'NetworkSecurityGroup'. Cannot convert the "#{Name=Any}" value of type "System.Management.Automation.PSCustomObject" to type "Microsoft.WindowsAzure.Commands.ServiceManagement.Network.NetworkSecurityGroup.Model.INetworkSecurityGroup".
It's quite clear what's happening, the issue is I don't know how to mock an object of type INetworkSecurityGroup. My expectation was initially to be no problem if mocking both Azure cmdlets.
I've also tried mocking Set-AzureNetworkSecurityRule using -MockWith:
Mock Set-AzureNetworkSecurityRule -MockWith {#{NetworkSecurityGroup='test-stuff'}}
with no luck.
Can anyone point me to the right direction ?
Thanks in advance
UPDATE with full Describe statement
First try
$environmentConfig = (Get-AzureEnvironmentConfig "Staging")
Describe 'Set-NetworkSecurityRuleFromObject' {
$role = ($environmentConfig.roles | Where { $_.role_name -eq 'Web'})
$nsgName = Get-NetworkSecurityGroupName -EnvironmentConfig $environmentConfig -Role $role
$rule = $role.nsg_rules.outbound[0]
Mock Get-AzureNetworkSecurityGroup
Mock Set-AzureNetworkSecurityRule
Set-NetworkSecurityRuleFromObject -NsgName -$nsgName -NsgRule $rule -NsgRuleType "Outbound"
It 'Should call mocked functions at least once' {
Assert-MockCalled Get-AzureNetworkSecurityGroup -Times 1 -Scope Describe
Assert-MockCalled Set-AzureNetworkSecurityRule -Times 1 -Scope Describe
}
}
Associated PS module function call:
Get-AzureNetworkSecurityGroup -Name $NsgName |
Set-AzureNetworkSecurityRule -Name $NsgRule.name `
-Type $NsgRuleType `
# More parameter initialization here ...
-Protocol $NsgRule.protocol |
Format-List -Property Name,Location,Label
Second try, another implementation of PS function that didn't work:
$nsg = Get-AzureNetworkSecurityGroup -Name $NsgName
$nsg |
Set-AzureNetworkSecurityRule -Name $NsgRule.name `
-Type $NsgRuleType `
# More parameter initialization here ...
-Protocol $NsgRule.protocol |
Format-List -Property Name,Location,Label
Third try
Describe 'Set-NetworkSecurityRuleFromObject' {
[ ... ]
Mock Get-AzureNetworkSecurityGroup { return [PSCustomObject] #{ Name='Any' } }
Mock Set-AzureNetworkSecurityRule
Set-NetworkSecurityRuleFromObject -NsgName -$nsgName -NsgRule $rule -NsgRuleType "Outbound"
It 'Should call mocked functions at least once' {
Assert-MockCalled Get-AzureNetworkSecurityGroup -Times 1 -Scope Describe
Assert-MockCalled Set-AzureNetworkSecurityRule -Times 1 -Scope Describe
}
}
This is still one of the most challenging things to do with Pester. You need to mock Get-AzureNetworkSecurityGroup in such a way that it returns a valid object to be passed to Set-AzureNetworkSecurityGroup later, and figuring out just how to do that can sometimes be tricky. In this specific case, it's not too bad:
Mock Get-AzureNetworkSecurityGroup {
return New-Object Microsoft.WindowsAzure.Commands.ServiceManagement.Network.NetworkSecurityGroup.Model.SimpleNetworkSecurityGroup('Any', 'someLocation', 'someLabel')
}
Since this function parameter is an interface type, you could also write a quick class in C# (or in PowerShell v5) which implements that interface; sometimes this might be more desirable, if the actual class starts doing "stuff" as soon as you create an instance. However, this SimpleNetworkSecurityGroup class doesn't really do anything other than hold data, and it's safe to use as-is. (Verified with ILSpy.)