Writing tests for PowerShell functions - powershell

I have the following function (the function is an auxiliary function of an another function and it works correctly):
function Get-UserManager {
[CmdletBinding()]
param (
[pscredential] $credential,
[ValidateSet('ID')][string]$searchType,
[string]$searchString,
)
try {
$reply = Invoke-RestMethod -Method Get -Uri $full_uri -Credentia $cred
} catch {
Write-Verbose "Couldn't connect to the End Point"
Write-Debug "$($_.Exception)"
return $userListObject
}
$reply.elements | ForEach-Object {
return $_
}
}
I am required to write a PowerShell test for the following function (The test must include all possible outputs, because I need the code coverage to be 100%).
Can someone please help me how do I write a PowerShell test that can check all the possible outputs of this function?
The test should be like this:
$moduleRoot = Resolve-Path "$PSScriptRoot\.."
$moduleName = Split-Path $moduleRoot -Leaf
$cred = Get-Credential
Describe "Demonstarting Code Coverage of: $moduleName" {
It "Calls Function: get-UserManager" {
{Get-UserManager -credential $cred -searchType ID -searchString
'12345' -delimiter} | Should Be $userListObject
}
}

I assume you're using Pester, which is a Behaviour-Driven Development (BDD) framework. That is, it is designed to help you verify the behaviour of your code.
Ideally, you'd design the tests first according to the specification, then write the code, but as you already have the code, you'll need to think about possible ways it could be used and how you expect it to behave in each case. For example, looking at your code, what do you expect to happen if $searchString is empty or invalid credentials are passed? How can you test this actually happens?
Incidentally, code coverage is related to the execution paths in your code and just because you have 100% coverage, doesn't mean you have completely tested the code. For example, consider this basic function:
function Get-Product {
Param (
$Param1,
$Param2
)
return $Param1 * $Param2
}
A single test that calls, say, Get-Product -Param1 12 -Param2 3 will have 100% code coverage as it tests all possible paths in the code, but it doesn't tell me how my code handles, for example, $Param1 being a string (e.g. "12") or one parameter is negative, etc, so I haven't really tested it thoroughly.

Your code is currently non-functional, I assume because you reduced it to share on StackOverflow but have left some key elements out. For example $full_uri and $userListObject aren't populated but are used in the function and you have an extra comma in your param block.
That being said, you probably want to take the approach of using Mocking to simulate parts of your script so you can force different behaviour to occur and visit every path in order to get 100% code coverage. For example you need a test where the API returns an exception and enters your Catch block. That might look like this:
Describe "Demonstarting Code Coverage of: $moduleName" {
Context 'Unable to connect to the endpoint' {
Mock Invoke-RestMethod { Throw 'Endpoint unavailable' }
Mock Write-Verbose
Mock Write-Debug
It 'Should enter the catch block when the endpoint returns an error' {
Get-UserManager -Verbose -Debug
Assert-MockCalled Write-Verbose -Times 1 -Exactly
Assert-MockCalled Write-Debug -Times 1 -Exactly
}
}
}
If you're completely new to Pester, Mocking can be a tricky topic to get your head around at first. I recommend doing some learning on Pester first. I did a talk on Getting Started with Pester at PSDay last year that you might find informative.

Related

Testing for mandatory parameters with Pester

I'm trying to figure out how to have Pester test for parameters that are missing:
Find-Waldo.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
Describe 'Mandatory paramters' {
it 'ComputerName' {
{
$Params = #{
#ComputerName = 'MyPc'
ScriptName = 'Test'
}
. "$here\$sut" #Params
} | Should throw
}
}
Find-Waldo.ps1
Param (
[Parameter(Mandatory)]
[String]$ComputerName,
[String]$ScriptName
)
Function Find-Waldo {
[CmdletBinding()]
Param (
[String]$FilePath
)
'Do something'
}
Every time I try to assert the result or simply run the test, it will prompt me for the ComputerName parameter instead of failing the test.
Am I missing something super obvious here? Is there a way to test for the presence of mandatory parameters?
Per the comments from Mathias, you can't really test for whether a Mandatory parameter is missing because PowerShell prompts for it rather than throwing an error. Per the comment he linked to from the Pester team you could use Get-Command to test for the Mandatory parameter setting in the script (assuming it is the only parameter attribute set for that variable)
((Get-Command "$here\$sut").Parameters['ComputerName'].Attributes.Mandatory | Should Be $true
An alternative option would be to not use Mandatory parameters in this instance, and instead have a script block that does a Throw as the default value of the parameter:
Param (
[String]$ComputerName = $(Throw '-ComputerName is required'),
[String]$ScriptName
)
If the script is always used as part of an automated process (instead of via user execution) this might be preferred as it allows you to control/capture its behavior and avoids it getting stuck during execution. You can then test the script as you had originally proposed:
Describe 'Mandatory paramters' {
it 'ComputerName' {
{
$Params = #{
#ComputerName = 'MyPc'
ScriptName = 'Test'
}
. "$here\$sut" #Params
} | Should throw '-ComputerName is required'
}
}
Although the accepted answer indicates that this isn't possible, it actually is possible. Here is the solution that I developed to solve for this problem.
It 'Should fail when no priority is specified, for a valid process name' {
{
$ScriptBlock = {
Import-Module -Name $args[0]
Set-ProcessPriority -Name System
}
Start-Job -ScriptBlock $ScriptBlock -ArgumentList $HOME/git/ProcessPriority/src/ProcessPriority | Wait-Job | Receive-Job
} | Should -Throw
}
What you'll notice from the above example is:
🚀 The code being tested has been wrapped in a PowerShell ScriptBlock
🚀 We invoke a PowerShell background job, containing the test code
🚀 We wait for the background job to complete, and then receive the results
🚀 If you run the Get-Job command, you'll notice that there is a job in the Blocked status
The exception that's thrown by the background job is similar to the following:
The Wait-Job cmdlet cannot finish working, because one or more jobs are blocked waiting for user interaction. Process interactive job output by using the Receive-Job cmdlet, and then try again.
You'll notice that I hard-coded the filesystem path to the module. I am not sure how to pass this as an argument into the "outer" ScriptBlock that Pester is invoking for us. Perhaps someone has a suggestion on how to accomplish that final piece of the puzzle.
What's uniquely interesting about PowerShell background jobs is that you can actually resume a job in the Blocked status, and it will prompt you for input, even though it threw the earlier exception.

Powershell workflow function issue

I´m trying to reuse code on my SMA runbooks but everything I try to put inside a function doesn´t seem to work as expected.
For example, If I do this it works and returns the username of the credential:
workflow RB_Test
{
$credent = Get-AutomationPSCredential -Name "CRED_TESTE"
$var = $credent.Username
"result = ${var}"
}
Output:
But if I turn into this it doesn't work anymore (returns null):
workflow RB_Test
{
function FN_Test
{
$credent = Get-AutomationPSCredential -Name "CRED_TESTE"
$var = $credent.Username
"result = ${var}"
}
FN_Test
}
Output:
I've tried different things but without success. The debug/verbose screen don't return anything different. That also doesn't work:
Inlinescript {
. FN_Test
}
My goal would be to put several functions into a separate module and then import it on my runbooks for reusability but this really seems not to work.
This is a runbook (powershell workflow) created in the Service Management Automation (SMA).
I've read that there are some restrictions with Powershell workflow compared to pure Powershell but I am not sure if I am hitting one of them:
https://blogs.technet.microsoft.com/heyscriptingguy/2013/01/02/powershell-workflows-restrictions/
Thanks
Here's what I've had to do to get functions to work:
workflow FunctionTest {
function log {
param(
[string]$Message
)
Write-Output $Message
Write-Output "Filename: $Filename"
Write-Output "using:Filename: $using:Filename"
Write-Output "workflow:Filename: $workflow:Filename"
Write-Output "----"
## Under what conditions is 'global' used? Can't be used in a workflow...Hey Scripting Guy!
}
workflow DoSomething {
param(
[string]$Filename
)
log "Starting DoSomething"
}
$Filename = "LogFile_2017.csv"
log "Starting workflow"
## Variables need to be passed into workflow from parent-workflow
DoSomething -Filename $Filename
log "End workflow"
}
FunctionTest
I found you need to define your functions before using them. The tricky part was discovering that you have to pass your variables into the child-workflow.
The scope of the variables takes some getting used to.

echo in while loop get's added to my return value

I wasn't sure how to describe this problem in the title so here goes.
I call a function from a script in another script. In that function i have a while loop that basically keeps looping through a set of ip's and looks up their hostname. when the while loop times out or we have all the host names.
it returns the hostnames.
My problem is that the return value contains every single Write-Host i'm doing in that function.
i know it's because Write-Host puts stuff on the pipeline and the return just returns whatever it has.
How do i go about fixing this?
The entire script i run get's logged in a log file which is why i want to have some verbose logging.
| out-null on write-host fixes the issue but it doesn't print the write-host values in the script.
in main.psm1 i have a function like so:
$nodes = #("ip1", "ip2", "ip3", "ip4")
$nodesnames = DoStuff -nodes $nodes
then in functions.psm1 i have functions like:
Function DoStuff
{
param($nodes)
$timeout = 300
$timetaken = 0
$sleepseconds = 5
$nodenames = #("$env:COMPUTERNAME")
while(($nodenames.count -lt $nodes.count) -and ($timetaken -lt $timeout))
{
try
{
Write-Host "Stuff"
foreach($node in $nodes)
{
$nodename = SuperawesomeFunction $node
Write-Host "$nodename"
if($nodenames -notcontains $nodename)
{
$nodenames += #($nodename)
}
}
}
catch
{
Write-Host "DoStuff Failed because $_"
}
Start-Sleep $sleepseconds
$timetaken += $sleepseconds
}
return $nodenames
}
Function SuperawesomeFunction
{
param($node)
$nodename = [System.Net.Dns]::GetHostEntry("$node")
return $nodename
}
Thanks.
So the answer is, your function is working like it is by design. In PowerShell a function will return output in general to the pipeline, unless specifically directed otherwise.
You used Echo before, which is an alias of Write-Output, and output is passed down the pipe as I mentioned before. As such it would be collected along with the returned $nodenames array.
Replacing Echo with Write-Host changes everything because Write-Host specifically tells PowerShell to send the information to the host application (usually the PowerShell Console or PowerShell ISE).
How do you avoid this? You could add a parameter specifying a path for a logfile, and have your function update the logfile directly, and only output the relevant data.
Or you can make an object with a pair of properties that gets passed back down the pipe which has the DNS results in one property, and the errors in another.
You could use Write-Error in the function, and set it up as an advanced function to support -errorvariable and capture the errors in a separate variable. To be honest, I'm not sure how to do that, I've never done it, but I'm 90% sure that it can be done.

Recursive -Verbose in Powershell

Is there an easy way to make the -Verbose switch "passthrough" to other function calls in Powershell?
I know I can probably search $PSBoundParameters for the flag and do an if statement:
[CmdletBinding()]
Function Invoke-CustomCommandA {
Write-Verbose "Invoking Custom Command A..."
if ($PSBoundParameters.ContainsKey("Verbose")) {
Invoke-CustomCommandB -Verbose
} else {
Invoke-CustomCommandB
}
}
Invoke-CustomCommandA -Verbose
It seems rather messy and redundant to do it this way however... Thoughts?
One way is to use $PSDefaultParameters at the top of your advanced function:
$PSDefaultParameterValues = #{"*:Verbose"=($VerbosePreference -eq 'Continue')}
Then every command you invoke with a -Verbose parameter will have it set depending on whether or not you used -Verbose when you invoked your advanced function.
If you have just a few commands the do this:
$verbose = [bool]$PSBoundParameters["Verbose"]
Invoke-CustomCommandB -Verbose:$verbose
I began using KeithHill's $PSDefaultParameterValues technique in some powershell modules. I ran into some pretty surprising behavior which I'm pretty sure resulted from the effect of scope and $PSDefaultParameterValues being a sort-of global variable. I ended up writing a cmdlet called Get-CommonParameters (alias gcp) and using splat parameters to achieve explicit and terse cascading of -Verbose (and the other common parameters). Here is an example of how that looks:
function f1 {
[CmdletBinding()]
param()
process
{
$cp = &(gcp)
f2 #cp
# ... some other code ...
f2 #cp
}
}
function f2 {
[CmdletBinding()]
param()
process
{
Write-Verbose 'This gets output to the Verbose stream.'
}
}
f1 -Verbose
The source for cmdlet Get-CommonParameters (alias gcp) is in this github repository.
How about:
$vb = $PSBoundParameters.ContainsKey('Verbose')
Invoke-CustomCommandB -Verbose:$vb

Alternative to Throwing Param Exceptions in PowerShell?

Bottom Line Up Front
I'm looking for a method to validate powershell (v1) command line parameters without propagating exceptions back to the command line.
Details
I have a powershell script that currently uses param in conjunction with [ValidateNotNullOrEmpty] to validate command line paramaters:
param(
[string]
[ValidateNotNullOrEmpty()]$domain = $(throw "Domain (-d) param required.")
)
We're changing the paradigm of error handling where we no longer want to pass exceptions back to the command line, but rather provide custom error messages. Since the param block can not be wrapped in a try catch block, i've resorted to something like the following:
param(
[string]$domain = $("")
)
Try{
if($domain -like $("")){
throw "Domain (-d) param required."
}
...
}Catch{
#output error message
}
My concern is that we're bypassing all of the built-in validation that is available with using param. Is my new technique a reasonable solution? Is there a better way to validate command line params while encapsulating exceptions within the script? I'm very much interested in see how PowerShell professionals would handle this situation.
Any advice would be appreciated.
You can write a custom validation script. Give this parameter a try.
Param(
[ValidateScript({
If ($_ -eq $Null -or $_ -eq "") {
Throw "Domain (-d) param required."
}
Else {
$True
}
})][string]$Domain
)
As I mentioned in a comment: more I read your description, more I come to the conclusion that you should not worry about "bypassing all built-in validation". Why? Because that's exactly your target. You want to bypass it's default behavior, so if that's what you need and have to do - than just do it. ;)
One way is to use default parameters like this [from msdn] -
Function CheckIfKeyExists
{
Param(
[Parameter(Mandatory=$false,ValueFromPipeline=$true)]
[String]
$Key = 'HKLM:\Software\DoesNotExist'
)
Process
{
Try
{
Get-ItemProperty -Path $Key -EA 'Stop'
}
Catch
{
write-warning "Error accessing $Key $($_.Exception.Message)"
}
}
}
So, here, if you try calling the function without passing any parameters, you will get warning what you have defined in your try/catch block. And, you are not using any default validation attributes for that. You should always assume that you will encounter an error, and write code that can survive the error. But the lesson here is if you implement a default value, remember that it is not being validated.
Read more here