Testing PowerShell with Pester

The content below is taken from the original (Testing PowerShell with Pester), to continue reading please visit the site. Remember to respect the Author & Copyright.

PowerShell-Text-Purple-hero

PowerShell-Text-Purple-hero

If you are an experienced PowerShell user, chances are you have heard of Pester. This is an open source project that Microsoft started shipping as part of Windows 10. I’m not going to try and teach Pester here, although it really isn’t that difficult to pick up. But I wanted to show you some ways to use Pester that you might not have considered.

Pester is typically designed for software testing. You build a test script to run through different parts of your code and Pester validates it. This is a quick way to verify you haven’t broken something while introducing something new.

A traditional Pester test

A traditional Pester test (Image Credit: Jeff Hicks)

But there’s no reason we can’t use the Pester logic to test other things. Perhaps that status of a critical server. The centerpiece of Pester is a logical test of “If some condition meets some test it should be some value”. It’s not that difficult to write a test that says “the DNS service should be running.” Here’s a simple Pester test to validate the state of my primary Hyper-V server.

#requires -version 5.0

$computername = "CHI-P50"

Describe $Computername {

It "should have Hyper-V Feature installed" {
    Get-windowsFeature -Name Hyper-V -ComputerName $Computername | Should Be $True
}

It "Hyper-V service should be running" {
    $s = Get-Service -Name vmms -ComputerName $computername
    $s.status | Should Be "running"
}

It "DNS service should be running" {
    $s = Get-Service -Name dns -ComputerName $computername
    $s.status | Should Be "running"
}

It "Should have 25% free space on drive C:" {
    $c = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "deviceid = 'c:'" -ComputerName $computername
    ($c.FreeSpace/$c.size)*100 | Should BeGreaterThan 25
}

It "Should have 10% free space on drive E:" {
    $e = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "deviceid = 'e:'" -ComputerName $computername
    ($e.FreeSpace/$e.size)*100 | Should BeGreaterThan 10
}

}

I could run this as a regular PowerShell script. But I prefer to use the Invoke-Pester cmdlet.

Invoking a Pester test script

Invoking a Pester test script (Image Credit: Jeff Hicks)

By using Invoke-Pester I can pass the results to the pipeline, output the results to XML and even specify what tests to run if I’ve named any of my tests. The benefit of using Pester is that you can automate the process running the test and taking action should there be any failures.

To test this, I’ll modify one of my tests so that it will result in a failure.

A Pester Test Failure

A Pester test failure (Image Credit: Jeff Hicks)

The failure is pretty easy to pick out. I also used the -Passthru parameter so you can see what kind of output to expect. I can then automate code like this which will email me failures.

Invoke-Pester c:\scripts\pester-chi-p50.ps1 -PassThru | 
Select -ExpandProperty TestResult | Where {-not $_.passed} |
foreach {
    Send-MailMessage -Subject "$($_.Describe) Test Failure" -body ($_ | Out-String)
}

Failure email notice

Failure email notice (Image Credit: Jeff Hicks)

I think that’s pretty slick. But it gets even better.

Since we’re talking PowerShell, you can use it to dynamically build your Pester tests. Here’s a test file that dynamically generates the same tests but with different expectations per server.

#use pester to validate servers or other infrastructure

<#
 Read in data to test per server. Could be read from an XML file

 Invoke-Pester <this file>
#>

$all = [pscustomobject]@{
Computername = "CHI-P50"
Services = @{Running = "vmms","vmcompute"},@{Stopped= "RemoteRegistry","Spooler"}
Features = @{Installed = "Hyper-V","Containers","Windows-Server-Backup"},@{NotInstalled = "Direct-Play","Internet-Print-Client"}
Versions = @{PowerShell = 5; Windows = 2016}
},
[pscustomobject]@{
Computername = "CHI-DC04"
Services = @{Running ="DNS","ADWS","KDC","NetLogon"},@{Stopped= "RemoteRegistry","Spooler"}
Features = @{Installed = "DNS","AD-Domain-Services","Windows-Server-Backup"},@{NotInstalled = "SMTP-Server","Internet-Print-Client"}
Versions = @{PowerShell = 5; Windows = 2012}
},
[pscustomobject]@{
Computername = "CHI-HVR2"
Services = @{Running ="vmms"},@{Stopped= "RemoteRegistry"}
Features = @{Installed = "Hyper-V","Windows-Server-Backup"},@{NotInstalled = "SMTP-Server","Internet-Print-Client"}
Versions = @{PowerShell = 4; Windows = 2012}
}

foreach ($item in $all) {

    Describe $($item.Computername) -Tags $item.Computername {

        $computername = $($item.Computername)
        $ps = New-PSSession -ComputerName $computername
        $cs = New-CimSession -ComputerName $computername
    
        It "Should be pingable" {
            Test-Connection -ComputerName $computername -Count 2 -Quiet | Should Be $True
        }

        It "Should respond to Test-WSMan" {
            {Test-WSMan -ComputerName $computername -ErrorAction Stop} | Should Not Throw
        }

    Context Features {

    $installed = Get-WindowsFeature -ComputerName $computername | Where Installed
    $Features = $($item.Features.Installed)  
    foreach ($feature in $features) {

       It "Should have $feature installed" {
            $installed.Name -contains $feature | Should Be $True
        }
    
    }

    $NotFeatures = $($item.features.notinstalled)
            foreach ($feature in $Notfeatures) {

               It "Should NOT have $feature installed"  {
                   $installed.Name -contains $feature | Should Be $False
                }
    
             }
    } #features

    Context Services  {

    $Stopped = $($item.services.Stopped)
    $Running = $($item.services.Running)

    $all = Invoke-Command { Get-Service } -session $ps
    foreach ($item in $Stopped) {
  
      It "Service $item should be stopped" {
        $all.where({$_.name -eq $item}).status | Should Be "Stopped"
      }

    }

    foreach ($item in $Running) {
  
      It "Service $item should be running" {
        $all.where({$_.name -eq $item}).status | Should Be "Running"
      }

    }

    } #services

    Context Versions {
        $winVer = $($item.versions.Windows)
        It "Should be running Windows Server $winVer" {
            (Get-CimInstance win32_operatingsystem -cimSession $cs).Caption | Should BeLike "*$winver*"
        }

        $psver = $($item.versions.powershell)
        It "Should be running PowerShell version $psver" {
            Invoke-Command { $PSVersionTable.psversion.major } -session $ps | Should be $psver
        }
    } #versions

    Context Other {
        It "Security event log should be at least 16MB in size" {
            ($cs | Get-CimInstance -ClassName win32_NTEVentlogFile -filter "LogFileName = 'Security'").FileSize | Should beGreaterThan 16MB
        }
        
        It "Should have C:\Temp folder" {
            Invoke-Command {Test-Path C:\Temp} -session $ps | Should Be $True
        }    
    } #other

    $ps | Remove-PSSession
    $cs | Remove-CimSession

    } #describe

} #foreach

I’ve hard coded the input values, but you could just as easily input them from an external source such as XML.

Testing server infrastructure with Pester

Testing server infrastructure with Pester (Image Credit: Jeff Hicks)

And, of course, I could build a response script to take remedial action for failures. Depending on what you are testing, if you configured the server with DSC, you have similar testing options. Although with Pester I can test for more intangible items like free disk space or free memory.

Sponsored

Or, here’s a simple Pester file for testing Active Directory and my domain controllers.

#requires -version 5.0
#requires -Module ActiveDirectory, DNSClient


<#

Use Pester to test Active Directory

Last updated: July 5, 2016

#>


$myDomain = Get-ADDomain
$DomainControllers = $myDomain.ReplicaDirectoryServers
$GlobalCatalogServers = (Get-ADForest).GlobalCatalogs

Write-Host "Testing Domain $($myDomain.Name)" -ForegroundColor Cyan
Foreach ($DC in $DomainControllers) {

    Describe $DC {

        Context Network {
            It "Should respond to a ping" {
                Test-Connection -ComputerName $DC -Count 2 -Quiet | Should Be $True
            }

            #ports
            $ports = 53,389,445,5985,9389
            foreach ($port in $ports) {
                It "Port $port should be open" {
                #timeout is 2 seconds
                [system.net.sockets.tcpclient]::new().ConnectAsync($DC,$port).Wait(2000) | Should Be $True
                }
            }

            #test for GC if necessary
            if ($GlobalCatalogServers -contains $DC) {
                It "Should be a global catalog server" {
                    [system.net.sockets.tcpclient]::new().ConnectAsync($DC,3268).Wait(2000) | Should Be $True
                }
            }
            
            #DNS name should resolve to same number of domain controllers
            It "should resolve the domain name" {
             (Resolve-DnsName -Name globomantics.local -DnsOnly -NoHostsFile | Measure-Object).Count | Should Be $DomainControllers.count
            }
        } #context
    
        Context Services {
            $services = "ADWS","DNS","Netlogon","KDC"
            foreach ($service in $services) {
                It "$Service service should be running" {
                    (Get-Service -Name $Service -ComputerName $DC).Status | Should Be 'Running'
                }
            }

        } #services

        Context Disk {
            $disk = Get-WmiObject -Class Win32_logicaldisk -filter "DeviceID='c:'" -ComputerName $DC
            It "Should have at least 20% free space on C:" {
                ($disk.freespace/$disk.size)*100 | Should BeGreaterThan 20
            }
            $log = Get-WmiObject -Class win32_nteventlogfile -filter "logfilename = 'security'" -ComputerName $DC
            It "Should have at least 10% free space in Security log" {
                ($log.filesize/$log.maxfilesize)*100 | Should BeLessThan 90
            }
        }
    } #describe

} #foreach

Describe "Active Directory" {

    It "Domain Admins should have 5 members" {
        (Get-ADGroupMember -Identity "Domain Admins" | Measure-Object).Count | Should Be 5
    }
    
    It "Enterprise Admins should have 1 member" {
        (Get-ADGroupMember -Identity "Enterprise Admins" | Measure-Object).Count | Should Be 1
    }

    It "The Administrator account should be enabled" {
        (Get-ADUser -Identity Administrator).Enabled | Should Be $True
    }

    It "The PDC emulator should be $($myDomain.PDCEmulator)" {
      (Get-WMIObject -Class Win32_ComputerSystem -ComputerName $myDomain.PDCEmulator).Roles -contains "Primary_Domain_Controller" | Should Be $True
    }
}

Testing Active Directory with Pester

Testing Active Directory with Pester (Image Credit: Jeff Hicks)

As you can see, I have some disk space issues to sort out.

Sponsored

Pester is a skill I think every PowerShell professional needs to begin developing. Start with simple tests for your modules. Once you gain a better understanding of how to construct effective tests, you’ll realize there are many things you can test and your investment in learning PowerShell continues to pay off.

The post Testing PowerShell with Pester appeared first on Petri.