Pester tests for PowerShell modules

if you write regularly PowerShell modules, you have to test the functions in it.

i wrote some Pester tests, which are doing this tests for all functions in a PowerShell module:

  • the function has SYNOPSIS, DESCRIPTION and EXAMPLES
  • the function has cmdletbinding
  • the function has an OutputType defined
  • the function name starts with an approved verb
  • the function name has an common prefix
  • all parameters have a help text
  • all parameters have a type declaration
  • all variables inside the function has the same upper/lower case
#region declarations
    $TestsPath = Split-Path $MyInvocation.MyCommand.Path
    $ScriptName = Split-Path $MyInvocation.MyCommand.Definition -Leaf
    $FunctionName = @( $ScriptName -split '\.' )[0]

    Clear-Host

    $RootFolder = (get-item $TestsPath).Parent

    Push-Location -Path $RootFolder.FullName

    Set-Location -Path $RootFolder.FullName

    $ModulePath = Get-ChildItem -Filter "*.psm1"
    Import-Module $ModulePath.FullName -Force

    $Functions = Get-Command -Module $ModulePath.BaseName -CommandType Function
    $CommonPrefix = 'CommonPrefix'
#endregion declarations

#region Pester tests
    foreach ( $FunctionName in @( $Functions.Name | Sort-Object ) ) {
        Describe "default tests for $( $FunctionName )" {
            $command = Get-Command -Name $script:FunctionName -All
            $help = Get-Help -Name $script:FunctionName
            $Ast = $command.ScriptBlock.Ast

            <#
            @( $Ast.FindAll( { $true } , $true ) ) | Where-Object { $_.Extent.Text -eq '[cmdletbinding()]' } | select *
            @( $Ast.FindAll( { $true } , $true ) ) | Group-Object TypeName
            @( $Ast.FindAll( { $true } , $true ) ) | Out-Gridview
            #>
            $Verb = @( $script:FunctionName -split '-' )[0]
            It "verb '$( $Verb )' should be approved" {
                ( $Verb -in @( Get-Verb ).Verb ) | Should -Be $true
            }

            try {
                $FunctionPrefix = @( $script:FunctionName -split '-' )[1].Substring( 0, $CommonPrefix.Length )
            }
            catch {
                $FunctionPrefix = @( $script:FunctionName -split '-' )[1]
            }
            it "function Noon should have the Prefix '$( $CommonPrefix )'" {
                $FunctionPrefix | Should -Be $CommonPrefix
            }

            It "Synopsis should exist" {
                ( $command.ScriptBlock -match '.SYNOPSIS' ) | Should -Be $true
            }
            It "Description should exist" {
                ( [string]::IsNullOrEmpty( $help.description.Text  ) ) | Should -Be $false
            }
            It "Example should exist" {
                [boolean]( $help.examples ) | Should -Be $true
            }
            It "[CmdletBinding()] should exist" {
                [boolean]( @( $Ast.FindAll( { $true } , $true ) ) | Where-Object { $_.TypeName.Name -eq 'cmdletbinding' } ) | Should -Be $true
            }
            It "[OutputType] should exist" {
                [boolean]( @( $Ast.FindAll( { $true } , $true ) ) | Where-Object { $_.TypeName.Name -eq 'OutputType' } ) | Should -Be $true
            }
            Context "parameters" {
                $DefaultParams = @( 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable')
                foreach ( $p in @( $command.Parameters.Keys | Where-Object { $_ -notin $DefaultParams } | Sort-Object ) ) {
                    It "help-text for paramater '$( $p )' should exist" {
                        ( $p -in $help.parameters.parameter.name ) | Should -Be $true
                    }

                    $Declaration = ( ( @( $Ast.FindAll( { $true } , $true ) ) | Where-Object { $_.Name.Extent.Text -eq "$('$')$p" } ).Extent.Text -replace 'INT32', 'INT' )
                    $VariableType = ( "\[$( $command.Parameters."$p".ParameterType.Name )\]" -replace 'INT32', 'INT' )
                    $VariableTypeFull = "\[$( $command.Parameters."$p".ParameterType.FullName )\]"
                    $VariableType = $command.Parameters."$p".ParameterType.Name -replace 'INT32', 'INT'
                    It "type '[$( $command.Parameters."$p".ParameterType.Name )]' should be declared for parameter '$( $p )'" {
                        ( ( $Declaration -match $VariableType ) -or ( $Declaration -match $VariableTypeFull ) ) | Should -Be $true
                    }
                }
            }
            Context "variables" {
                $code = $command.ScriptBlock
                $ScriptVariables = $code.Ast.FindAll( { $true } , $true ) |
                    Where-Object { $_.GetType().Name -eq 'VariableExpressionAst' } |
                    Select-Object -Property VariablePath -ExpandProperty Extent

                foreach ( $sv in @( $ScriptVariables | Select-Object -ExpandProperty Text -Unique | Sort-Object ) ) {
                    It "variable '$( $sv )' should be in same (upper/lower) case everywhere" {
                        [boolean]( $ScriptVariables | Where-Object { ( ( $_.Text -eq $sv ) -and ( $_.Text -cne $sv ) ) } ) | Should -Be $false
                    }
                }
            }
        }
    }
#endregion Pester tests