2

I would like to define stand-alone functions in my PowerShell script and be able to Pester test the functions without executing the rest of the script. Is there any way to do this without defining the functions in a separate file?

In the following pseudocode example, how do I test functionA or functionB without executing mainFunctionality?

script.ps1:

functionA
functionB
...
mainFunctionality 

script.Tests.ps1:

BeforeAll {
  . $PSScriptRoot/script.ps1 # This will execute the mainFunctionality, which is what I want to avoid
}
Describe 'functionA' { 
   # ... tests
}

I believe in Python, you can do this by wrapping your "mainFunctionality" inside this condition, so I am looking for something similar in Powershell.

if __name__ == '__main__':
    mainFunctionality

Ref: What does if __name__ == "__main__": do?

successhawk
  • 3,071
  • 3
  • 28
  • 44
  • 1
    By moving `mainFunctionality` into a separate file :) – Mathias R. Jessen Jan 26 '23 at 15:34
  • Use [AST](https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.language.ast) (`[System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$errors)[System.Management.Automation.Language.Parser]::ParseFile("$PSScriptRoot/script.ps1", [ref]$null, [ref]$null)`) to browse for the functions in your script and invoke them. – iRon Jan 26 '23 at 15:56
  • You could add an optional switch parameter to your script to skip "mainFunctionality". When including the script for testing, call it with the switch parameter. – zett42 Jan 26 '23 at 16:02
  • @zett42 I think this is the best solution so far. However, I was hoping there would be some solution that wasn't exposed by the "interface" of the script. – successhawk Jan 26 '23 at 17:04
  • @iRon can you provide an answer or link to an example? – successhawk Jan 26 '23 at 17:04
  • Another way without changing the interface of the script would be to simply check a unique variable. Test code then defines the variable before dot-sourcing the script. – zett42 Jan 26 '23 at 17:48

2 Answers2

1

Using the PowerShell Abstract Syntax Tree (AST) to just grab functionA and invoke it:

$ScriptBlock = {
    function functionA {
        Write-Host 'Do something A'
    }
    
    function functionB {
        Write-Host 'Do something A'
    }
    
    function mainFunctionality {
        # Do something
        functionA
        # Do something
        functionB
        # Do something
    }
    
    mainFunctionality
}

Using NameSpace System.Management.Automation.Language
$Ast = [Parser]::ParseInput($ScriptBlock, [ref]$null, [ref]$null) # or: ParseFile
$FunctionA = $Ast.FindAll({
    $Args[0] -is [ScriptBlockAst] -and $Args[0].Parent.Name -eq 'functionA'
}, $True)
Invoke-Expression $FunctionA.EndBlock

Do something A
iRon
  • 20,463
  • 10
  • 53
  • 79
  • 1
    Nice, but what about the dependencies of the function, e. g. a script variable or another function within the same script? – zett42 Jan 26 '23 at 18:12
  • @zett42, good point, it was just meant as an AST example. It is probably better to seach for the `mainFunctionality` caller and remove that from the scriptblock, see also: https://stackoverflow.com/a/74043318/1701026 – iRon Jan 26 '23 at 18:29
  • I clarified my question to state that I am talking about stand-alone functions, i.e. have no external dependencies... But I would hope that the solution would also apply to a function that calls other functions in the same script, as long as none of them depend on external state (i.e. externally scoped variables). However, I haven't looked yet, but maybe it is possible to mock script/global variables to account for these scenarios. – successhawk Jan 26 '23 at 19:23
1

You could use $MyInvocation.PSCommandPath to determine who invoked your script, its the closest I can think of to Python's if __name__ == '__main__':. This property will give you the absolute path of the caller. From there you can extract the script name, i.e. with Path.GetFileName and after you can determine what you want to do, for example, call mainFunctionality if the caller's name equals to main.ps1 or call mainFunctionality if the caller's name is not equal to script.Tests.ps1.

Here is a short example.

  • myScript.ps1
function A {
    "I'm function A"
}

function B {
    "I'm function B"
}

function mainFunctionality {
    "I'm function mainFunctionality"
}

A # Calls A
B # Calls B

# Call `mainFunctionality` only if my caller's name is `main.ps1`
if([System.IO.Path]::GetFileName($MyInvocation.PSCommandPath) -eq 'main.ps1') {
    mainFunctionality
}

Then if calling myScript.ps1 from main.ps1 you would see:

I'm function A
I'm function B
I'm function mainFunctionality

And if calling myScript.ps1 from anywhere else (console or other script with a different name) you would see:

I'm function A
I'm function B
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37