Pester: Use variables in -ForEach data driven tests with rich content - powershell

I write a module used for powershell-wrapping a commandline tool.
For my tests I had some variables defined in BeforeAll. Those vars should help in feeding the command with desired params and check them against the expected result, f.i.
BeforeAll {
$PORead=#{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp="-bla bla (bla:bla;fra:fra;tra:tra)"
}
Now I want to use these in an it segment to feed the function and check the result, like:
It "checks result" -ForEach (
#{Param=$PORead;Expected=$POReadExp}
) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}
Note: This is actually no working code - just to make clear what I want.
The problem is, that $Param and $Expected are $null, thus the current value of $PORead and $POReadExp are not assigned to $Param and $Expected.
I made it easy to demonstrate the problem - in my code I use arrays in the -ForEach hash assigned to $Param and $Expected.
Using plain strings, there is no problem and everything works fine.
I could use plain text and than use som switch construct to match my strings with the according variables from BeforeAll{}, but I thought there must be a more elegant way.
Using standard hashtables, the variable substition works fine.
I'd appreciate any idea.
To clarify some things:
The solution of Mark works somehow, but it needs the $PORead and $POReadExp to be set outside the BeforeAll-block (if you want to use -ForEach), or within the BeforeAll-block, but in that case you only can use one argument.
My plan was a bit different, so I'm making my point a bit clearer:
In the BeforeAll-block I wanted a bunch of definitions for params and outcomes
BeforeAll {
$PORead = #{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
$PODelete = #{
Name = "Paul"
Permission ="Delete"
Inheritance = "ThisFolderOnly"
}
$POReadResult = "add parameters"
$PODeleteResult = "delete parameters"
}
Now I want to test a whole bunch of combinations, feeding different combinations to the function and checking, if the combination of results match:
It "Test if all combinations works" -ForEach(
#{FunctionParam=$PORead;Expected=$POReadExp}
#{FunctionParam=$PORead,$PORead;Expected=$POReadExp,$POReadExp}
#{FunctionParam=$PORead,$PODelete,$PORead,$PORead;Expected=$PORead,$PORead,$PORead,$PODelete}
) {
$result = Function-Call -Param $FunctionParam
$wanted = $expected -join(" ")
$result | Should -Be $wanted
}
I can realize this by placing the definitions out of any block, but it feels wrong in terms of pester coding guidelines.
Greets
Usul

The scopes for how to set test data with Pester v5 are quite confusing. What I've found is that anything you set in a BeforeAll or BeforeEach is available within the It. However if you want to set values to the -ForEach (or its alias -TestData) you need to do it as part of or before the It, outside of the BeforeEach or BeforeAll.
As such I think your tests will work if you do this:
$PORead = #{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp = "-bla bla (bla:bla;fra:fra;tra:tra)"
It "checks result" -ForEach (
#{Param=$PORead;Expected=$POReadExp}
) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}
Or alternatively (if you just need to perform a single test):
BeforeAll {
$PORead=#{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp="-bla bla (bla:bla;fra:fra;tra:tra)"
}
It "checks result" {
$result = FunctionCall -Parameter1 $PORead
$result | Should -Be $POReadExp
}
If you want to avoid using a variable that is not within one of the Pester code blocks, then you should do this (which is in line with the examples given in the Pester documentation on Data Driven Tests):
It "checks result" -ForEach (
#{
Param = #{
Name = "Paul"
Permission = "Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
Expected = "-bla bla (bla:bla;fra:fra;tra:tra)"
}) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}

Related

Create a wrapper for functions in powershell

I want to wrap a function, that is to create a new function such that it would automatically pass some arguments to the old function, like python's partial functions. The arguments passed are the ones defined in the callee and not the caller. The important thing is that I don't want to refer to each of them explicitly (define them twice).
That is really done to save typing-in the same flags to complicated functions while allowing customization.
For example, in python, I would do:
call_with_x=partial(call,x=1)
or maybe use **kw and pass it to the callee in some cases .
This is my best try (based on Wrapper function in PowerShell: Pass remaining parameters) :
function Let
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$Option,
[parameter(mandatory=$false, position=1, ValueFromRemainingArguments=$true)]$Remaining)
Get #Remaining
}
function Get
{
[CmdletBinding()]
Param([parameter(mandatory=$false, position=0)][string]$OptionA,
[parameter(mandatory=$true, position=1)][string]$OptionB)
Write-Host $OptionA, $OptionB
}
But Let -Option c -OptionA 1
Prints -OptionA 1
which is obviously not what I intended.
If you don't require "advanced" function features from CmdletBinding(), then you can get away with using $args for this:
# simple function parameters are positional and named, but not mandatory
function let {
Param($OptionA,$OptionB)
write-host "OptionA=$OptionA"
write-host "OptionB=$OptionB"
get #args
}
function get {
Param($OptionC,$OptionD)
write-host "OptionC=$OptionC"
write-host "OptionD=$OptionD"
}
# not necessary to name or include parameters
let -OptionA A B -OptionC C D
OptionA=A
OptionB=B
OptionC=C
OptionD=D
# named parameters get assigned first, so the order is not too big a deal either
# these produce the same results as above:
let B -OptionA A D -OptionC C
let -OptionD D -OptionC C A B
Any named parameters will not get positionally assigned to let
Any additional parameters, named or otherwise, will be forwarded to get
That was hard!
For this you would need to add a bit of code in DynmaicParams and begin section of the function.
It might be possible to do it in an Attribute
function x{
[WrapperFor(y)]
Might do it Later.
function Get
{
[CmdLetBinding()]
Param([parameter(mandatory=$false, position=0)][string]$OptionA,
[parameter(mandatory=$false, position=1)][string]$OptionB)
Write-Host "opta",$OptionA
Write-Host "optb",$OptionB
}
function Let
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$Option,[parameter(mandatory=$false, position=0)][string]$OptionB)
DynamicParam {
AddWrapper -For "Get" -To "Let"
}
Begin {
$params = GetRestOfParams "Get" $PSBoundParameters
}
Process {
Get #params
}
}
Needed code:
using namespace System.Management.Automation
function Empt
{
[CmdletBinding()]
Param([parameter(mandatory=$true, position=0)][string]$aaaa)
1
}
function AddWrapper([parameter(mandatory=$true, position=0)][string]$For,[parameter(mandatory=$true, position=1)][string]$To)
{
$paramDictionary = [RuntimeDefinedParameterDictionary]::new()
$paramset= $(Get-Command $For).Parameters.Values | %{[System.Management.Automation.RuntimeDefinedParameter]::new($_.Name,$_.ParameterType,$_.Attributes)}
$paramsetlet= $(Get-Command empt).Parameters.Keys
$paramsetlet+= $(Get-Command $To).ScriptBlock.Ast.Body.ParamBlock.Parameters.Name | %{ $_.VariablePath.UserPath }
$paramset | %{ if ( -not ($paramsetlet -contains $_.Name) ) {$paramDictionary.Add($_.Name,$_)}}
return $paramDictionary
}
function GetRestOfParams($dst,$params)
{
$dstorgparams=$(Get-Command $dst).Parameters.Keys
$z= $params
$z.Keys | %{ if ( -not ($dstorgparams -contains $_) ) {$z.Remove($_)} } | Out-Null
return $z
}

Defining parameters common to all functions within a PowerShell module

I am writing a PowerShell module, the functions inside this module have some parameters which will be re-used across all functions. Rather than copy-pasting the function definition each time I add a new function, I would like to define them at the top like a script variable and then insert them into each function, giving me a single place to update if they need to be changed.
Looking at how dynamic parameters are defined it seems like I should be able to define an object of that type and then reference it in the function definitions, but I can't find anything online giving me the correct syntax to do this.
Using PowerShell version 7.2
$Script:ReUsedParameters = param(
[Parameter()]
[String]$Name,
[Parameter()]
[Int]$Id
)
Function New-Command {
Param ($ReUsedParameters)
Write-Output "Name: $Name, ID: $ID"
}
For the sake of answering, you can store the runtime parameters definitions in a script block and then call it & inside the function's dynamicparam block.
I do not think this is a good idea nor I recommend using this. All functions should have their own repeated param blocks if needed.
$reusedParameters = {
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
# Since both parameters don't have any arguments (Mandatory, Position, ValueFromPipeline, etc..)
# you can use this one for both, otherwise, each dynamic parameter should have their own
# Parameter Declaration
[Parameter[]] $paramAttribute = [Parameter]::new()
$paramDictionary['Name'] = [System.Management.Automation.RuntimeDefinedParameter]::new('Name', [string], $paramAttribute)
$paramDictionary['Id'] = [System.Management.Automation.RuntimeDefinedParameter]::new('Id', [int], $paramAttribute)
return $paramDictionary
}
Function New-Command {
[CmdletBinding()] # `CmdletBinding` is Mandataroy here
param() # if the `param` block is empty
dynamicparam {
& $reusedParameters
}
end {
# Caveat: you can reference these parameters via $PSBoundParameters
# $Name and $Id are not part of the `param` block
# hence that wouldn't work here
"Name: {0}, ID: {1}" -f $PSBoundParameters['Name'], $PSBoundParameters['ID']
}
}
New-Command -Name asd -Id 123
As a declarative approach, you may turn the common parameters into class properties and have a single function parameter of the class type.
class MyReUsedParameters {
[String] $Name
[Int] $Id = 23
}
Function New-Command {
Param (
[MyReUsedParameters] $ReUsedParameters,
$AnotherParam
)
Write-Output "Name: $($ReUsedParameters.Name), ID: $($ReUsedParameters.ID)"
}
# Pass the common parameters as a hashtable which gets converted to
# MyReUsedParameters automatically.
New-Command -ReUsedParameters #{ Name = 'foo'; Id = 42 } -AnotherParam bar
# Alternatively pass the common parameters as a (typed) variable.
# PowerShell is able to deduce the argument name from the type.
$commonArgs = [MyReUsedParameters] #{ Name = 'Foo'; Id = 42 }
New-Command $commonArgs -AnotherParam bar
When passing a hashtable or PSCustomObject that has matching properties, it will automatically be converted to the class type.
You may even validate class properties similar to regular parameters. Most parameter validation attributes can be specified for class properties as well.
class MyReUsedParameters {
[ValidateNotNullOrEmpty()] [String] $Name
[Int] $Id = 23
# Constructor - required to apply validation
MyReUsedParameters( [Hashtable] $ht ) {
$this.Name = $ht.Name
$this.Id = $ht.Id
}
}
Function New-Command {
Param (
[Parameter(Mandatory)]
[MyReUsedParameters] $ReUsedParameters
)
Write-Output "Name: $($ReUsedParameters.Name), ID: $($ReUsedParameters.ID)"
}
# Causes an error (as expected), because Name property is missing
New-Command -ReUsedParameters #{ Id = 42 }

powershell workflow - parameter splatting not supported?

I'm scratching my head trying to figure out why parameter splatting won't work for a set of workflow runbooks executed in Azure Automation.
I've got 3 runbooks (super, sub, child) that are called in a nested fashion.
They all share (generally) the same parameters. In my real-world scenario, I have multiple sub & child runbooks and I want to use splatting so I could define a hashtable once and pass it as needed, without having to list out all the params.
I've also tried using the -PSParameterCollection parameter. However, in Azure Automation, this appears only to be available for the topmost runbook (which already works), as it throws a parameter not found error for it in the children. The Note section here says Workflows that are nested three levels deep do not support any common parameters, including workflow common parameters, which seems like it could be a problem, depending on how these runbooks are compiled.. but I don't think I can avoid that without limiting the reusability of the runbooks. I wouldn't expect called workflows to be compiled as nested workflows within the calling one.
The error I get is:
$newnumber = sub #splatter
~~~~~~~~~~
Could not find a parameter named '0'. Supported parameters are: Debug, ErrorAction, ....
Oddly enough, unlike what I'm seeing with these test runbooks, in my 'real-world' workflows, I don't get this error in the top-level runbook - only when calling the 'child' runbooks from within 'sub'.
workflow child
{
Param ([int]$val)
Write-Verbose "child: val ($val)"
$NewNumber = $val*2
$NewNumber
}
workflow childtwo
{
Param ([int]$val, [int]$valtwo)
Write-Verbose "childtwo: val ($val) valtwo ($valtwo)"
$NewNumber = $val * $valtwo
$NewNumber
}
workflow sub
{
Param ([int]$val)
Write-Verbose "sub: val ($val)"
$splatter = #{ val = $val }
# works
$NewNumber = child -val $splatter.val
# fail
$NewNumber = child #splatter
# works
$NewNumber = childtwo -val $splatter.val -valtwo 2
# fail
$NewNumber = childtwo ($splatter + #{ valtwo = 2 })
$mergeSplatter = ($splatter + #{ valtwo = 2 })
$NewNumber = childtwo #mergeSplatter
$twoSplatter = #{ val = $val; valtwo = 2 }
$NewNumber = childtwo #twoSplatter
$NewNumber
}
workflow super
{
Param([int]$val)
Write-Verbose "super: id is $val"
$splatter = #{ val = $val }
$newnumber = sub -val $val
$newnumber = sub #splatter # # or $ doesn't matter - fails the same
Write-Verbose "super: newnumber is $newnumber"
$newnumber
}
super 2
Please see https://technet.microsoft.com/en-us/library/jj574140.aspx?f=255&MSPPError=-21472173969:
Splatting is not permitted in workflow activities or in calls to workflows.
You will need to either not use splatting or switch the PowerShell Workflow runbook to a native PowerShell runbook.

Powershell: How to add null parameters in function call with Splatting

I have a function call implemented using Splatting.
$funtioncall= #{
Param1=$Param1;
Param2=$Param2;
Param3=$Param3;
Param4=$Param4;
Param5=$Param5;
}
function #functioncall
On a certain scenario i wish to add two more optional Parameters to the function call only if they are not null.
So i have $OptionalParam1 and $OptionalParam2
I currently have the below code to enable splatting as it will not allow nulls to be included in the Hash Table!
if(($OptionalParam1)-and($OptionalParam2))
{
$funtioncall= #{
Param1=$Param1;
Param2=$Param2;
Param3=$Param3;
Param4=$Param4;
Param5=$Param5;
OptionalParam1=$OptionalParam1;
OptionalParam2=$OptionalParam2;
}
}
else
{
$funtioncall= #{
Param1=$Param1;
Param2=$Param2;
Param3=$Param3;
Param4=$Param4;
Param5=$Param5;
}
}
function #functioncall
Is there a simpler way i can do this with Splatting?
This without splatting would be easier to implement and function call would look like below,( as i can have the parameter defined in the function to allow null )
function -Param1 $Param1 -Param2 $Param2 -Param3 $Param3 -Param4 $Param4 -Param5 $Param5 -OptionalParam1 $OptionalParam1 -OptionalParam2 $OptionalParam2
You should not have to change anything. $null is still a value so there is no reason to treat building of the variable $funtioncall differently at all
$param1 = "Awesome"
$OptionalParam1 = $null
$funtioncall= #{
Param1=$Param1;
OptionalParam1=$OptionalParam1;
}
So now the hashtable contains a null for OptionalParam1
Name Value
---- -----
OptionalParam1
Param1 Awesome
So as long as your function can handle to possibility of the param being null there will be no issue. I made a small function that displays those values.
This #funtioncall
Param1 is 'Awesome'
OptionalParam1 is ''

How to run if inline on PowerShell command

Suppose I have this function;
Function SomeCommand{
param(
[string]$Var1,
[string]$Var2
)
# Do Something
}
I could set it up like this;
Function SomeCommand{
param(
[string]$Var1,
[string]$Var2
)
if ($Var2){
Do-SomeOtherCommand -SomeParam1 $Var1 -SomeParam2 $Var2
} else {
Do-SomeOtherCommand -SomeParam1 $Var1
}
This works fine if I only have one optional parameter, but if I have two it gets harry. I would like to do something like this;
Function SomeCommand{
param(
[string]$Var1,
[string]$Var2,
[string]$Var3
)
Do-SomeOtherCommand -SomeParam1 $Var1 (if($Var2){-SomeParam2 $Var2}) (if($Var3){-SomeParam3 $Var3})
}
Is there a way to accomplish this?
You are probably looking for splatting. You can build up a hashtable with the parameters you wish to pass (and their values), then specify the whole thing in one shot:
function FuncB($param1, $param2)
{
"FuncB -- param1:[$param1] param2:[$param2]"
}
function FuncA($paramA, $paramB)
{
$args = #{}
if ($paramA){ $args['param1'] = $paramA }
if ($paramB){ $args['param2'] = $paramB }
FuncB #args
}
Test
FuncA 'first' 'second'
FuncA 'OnlyFirst'
FuncA -paramB 'OnlySecond'
# results
# FuncB -- param1:[first] param2:[second]
# FuncB -- param1:[OnlyFirst] param2:[]
# FuncB -- param1:[] param2:[OnlySecond]
Semicolons. PowerShell allows you to use semicolons as line terminators.
Write-Output 1;Write-Output 2;Write-Output 3;
Personally, I think it should be mandatory.
Also note that you can build up an arbitrary expression as a simple string, then use Invoke-Expression (alias iex) to invoke it inline.
function FuncB($param1, $param2)
{
"FuncB -- param1:[$param1] param2:[$param2]"
}
function FuncA($paramA, $paramB)
{
$funcBCall = "FuncB $(if($paramA){ "-param1 '$paramA'" }) $(if($paramB){ "-param2 '$paramB'" })"
iex $funcBCall
}
This approach is very hacky and brittle, though, so I wouldn't recommend it.