WavebreakmediaMicro - Fotolia

Pester tests help pinpoint infrastructure issues

The Pester testing framework gives IT pros a way to develop sophisticated and consistent testing routines that monitoring tools just can't duplicate.

Troubleshooting is a fact of life for the Windows administrator. Something is broken that prohibits an employee from doing important work, and it's your job to find out what's wrong and fix it fast.

There are many troubleshooting approaches. At one end, you have frenzied clicking around multiple tools desperately trying to spot something that can shed a clue. This hope-based approach is not the best. At the other end, you can employ a known set of tests that enable you to work methodically through the issues and get to the root of the problem. The advantage to the more methodical approach is that you get to the answer, but the downside is it requires multiple steps. The ideal approach combines the best of both worlds with an automated troubleshooting approach.

Enter the Pester module for PowerShell, which provides a testing framework for PowerShell code and infrastructure setups. The advantage of troubleshooting with Pester tests is that the tests always perform in a consistent manner, which means other members of the IT team can use them to run tests. One way to extend the concept is to add suggestions for possible remedies when a test fails to teach junior admins how to troubleshoot.

The anatomy of a common troubleshooting scenario

Consider a common troubleshooting scenario: A user can't connect to a server. You might try several tests and hope to see the following results. First, test the user's network card by testing the loopback address.

Test-Connection 127.0.0.1 -Quiet

Then, test the local machine's IP address.

Test-Connection 10.10.54.5 -Quiet

Test the server's address.

Test-Connection 10.10.54.30 -Quiet

Test the connection via the server's name. This also tests the DNS resolution from the client.

Test-Connection W19FS01 -Quiet

Figure 1 shows the expected results.

Testing network connectivity
Figure 1. Testing the network connectivity to the server named W19FS01 returns the expected results.

How could we go about automating a suite of tests of this sort? One way is to use the PowerShell Pester module. Windows PowerShell v5.1 has Pester version v3.4 installed; you must upgrade the version of Pester if you use Windows PowerShell or install the latest version if you work with PowerShell Core.

On Windows PowerShell, use the following.

Install-Module -Name Pester -Force -SkipPublisherCheck

This will install the latest version of Pester even though there is a version preinstalled with the OS.

On PowerShell Core, use the following.

Install-Module -Name Pester -Scope AllUsers -Force

Once you've installed Pester from the gallery, you can update it on any version of PowerShell with the following.

Update-Module -Name Pester -Force

PowerShell Core v6.2 has a problem of installing modules from the gallery into the C:\Users\<user name>\Documents\PowerShell\Modules folder by default. When you use Install -Module, you can override this behavior using the AllUsers scope. Update-Module doesn't have a Scope parameter, so the new version of the module ends up in the user area rather than the C:\Program Files\PowerShell\Modules folder. You should keep modules there to make them accessible to all users on the system. Until the PowerShell team resolves this issue, it may be best on PowerShell Core to delete old versions of the module and reinstall into the AllUsers scope rather than using Update-Module.

Building Pester tests with Windows PowerShell

In the following example, I use Windows PowerShell for the more polished version of Test-Connection and easier access to other modules. You can use the Windows Compatibility module for PowerShell Core to use the required modules.

Let's create the first test, pinging the loopback adapter.

## test loopback adapter
Describe 'Loopback Adapter' {
  It 'Loop back adapter should be pingable' {
    Test-Connection -ComputerName 127.0.0.1 -Quiet -Count 1 |
    Should Be $true
  }
}

The Describe keyword creates the test container with a name, analogous to defining and supplying a name to a function. The It keyword creates a test. The text following It gets echoed back when displaying the results. You can have multiple tests in a single container; for this article, I will restrict tests to one per container.

Within the It block, the syntax reduces to the following.

<test> | Should <desired result>

Our example is a test.

Test-Connection -ComputerName 127.0.0.1 -Quiet -Count 1

And here is a desired result.

Should Be $true

Most of the tests you'll design for troubleshooting purposes will have the following result.

Should Be <value>

The Pester documentation (about_Pester and about_should) explains how to create other tests.

When you run the test, you should see these results.

Describing Loopback Adapter
  [+] Loop back adapter should be pingable 1.04s

The first line echoes the container name and then each test in the container is run -- with success shown by [+] or a failure by [-]. The text from the It statement is echoed back with the time taken to run the test.

Let's add some more tests.

## test local adapter
Describe 'Local Adapter' {
  It 'Local adapter should be pingable' {
    Test-Connection -ComputerName 10.10.54.5 -Quiet -Count 1 |
    Should Be $true
  }
}
## test server adapter
Describe 'Server Adapter' {
  It 'Server adapter should be pingable' {
    Test-Connection -ComputerName W19FS01 -Quiet -Count 1 |
    Should Be $true
  }
}

The first test is for the local adapter, and the second test is for the server adapter. You'd probably want to test the default gateway in between the two tests, but my test lab doesn't have one.

Running the three tests gives these results.

Describing Loopback Adapter
  [+] Loop back adapter should be pingable 82ms

Describing Local Adapter
  [+] Local adapter should be pingable 74ms

Describing Server Adapter
  [-] Server adapter should be pingable 3.07s
    Expected $true, but got $false.
    20:     Should Be $true

The first two tests passed, but pinging the server adapter failed. Notice the results included the expected result and the actual result.

The test that pings the server adapter actually performs two tests. It tests the resolution of the name of the server to an IP address and then pings that IP address. You should only test one thing at a time to determine the problem more accurately.

The tests should be changed to the following.

## test loopback adapter
Describe 'Loopback Adapter' {
  It 'Loop back adapter should be pingable' {
    Test-Connection -ComputerName 127.0.0.1 -Quiet -Count 1 |
    Should Be $true
  }
}

## test local adapter
Describe 'Local Adapter' {
  It 'Local adapter should be pingable' {
    Test-Connection -ComputerName 10.10.54.5 -Quiet -Count 1 |
    Should Be $true
  }
}

## test server IP address
Describe 'Server IP address' {
  It 'Server IP address should be pingable' {
    Test-Connection -ComputerName 10.10.54.30 -Quiet -Count 1 |
    Should Be $true
  }
}

## test server name
Describe 'Server Name' {
  It 'Server name should be pingable' {
    Test-Connection -ComputerName W19FS01 -Quiet -Count 1 |
    Should Be $true
  }
}

The first and second tests remain the same, while I split the third test to check the server's IP address and then the resolution and ping. Running these tests produced the following results.

Describing Loopback Adapter
  [+] Loop back adapter should be pingable 104ms

Describing Local Adapter
  [+] Local adapter should be pingable 87ms

Describing Server IP address
  [-] Server IP address should be pingable 3.65s
    Expected $true, but got $false.
    21:     Should Be $true

Describing Server Name
  [-] Server name should be pingable 3.66s
    Expected $true, but got $false.
    29:     Should Be $true

The first two tests passed, but both the third and fourth tests failed.

How to fine-tune troubleshooting with Pester tests

Windows troubleshooting is often an iterative process; you find one issue and resolve it, only to uncover another issue.

Windows troubleshooting is often an iterative process; you find one issue and resolve it, only to uncover another issue. In our example, you'd ideally want the tests to stop after the first failure. The tests on the server failed because it's switched off. If you discover that you can't ping the server, then the next step would be to check if it's running.

If you run a file containing Pester tests, all the tests in the file will run even if some of the tests fail. To stop after the first failure, you must run the tests individually, which requires you to call the tests from another script.

Save the pester tests as PingTests.ps1 to a folder called TroubleShooting. The files containing the tests are in a subfolder called Tests.

Make a second script called Test-ServerPing.ps1 in the Troubleshooting folder with the following.

$tests = 'Loopback Adapter', 'Local Adapter', 'Server IP address', 'Server Name'

$data = foreach ($test in $tests) {
$result = $null
$result = Invoke-Pester -Script @{Path = 'C:\Scripts\TroubleShooting\Tests\PingTests.ps1'} -PassThru -TestName "$test" -Show None
 
$props = [ordered]@{
    'Test' = $result.TestResult.Name
    'Result' = $result.TestResult.Result
    'Failure Message' = $result.TestResult.FailureMessage
  }
 New-Object -TypeName PSObject -Property $props

if ($result.FailedCount -gt 0) {break}
}

$data | Format-Table -AutoSize -Wrap

These tests are from each of the Describe statements shown earlier. We use a foreach loop to run each test from the PingTests.ps1 file. The results from each test create an output object. If a failure occurs, the testing stops so you can see where the first failure occurred without spending time on further irrelevant -- at this stage -- testing.

 Running Test-ServerPing.ps1 produces these results.

Test                                 Result Failure  Message                 
----                                 ------ ------- -------                
Loop back adapter should be pingable Passed                               
Local adapter should be pingable     Passed                               
Server IP address should be pingable Failed Expected $true, but got $false.

Currently, the IP addresses and server name are hardcoded. Hardcoding the loopback adapter address is acceptable because it's always 127.0.0.1. The other tests must be made generic. The easiest way to change that is to create a variable in Test-PingServer.ps1 that stores the required information. That variable will be accessible by the Pester tests because they run in a child scope of Test-PingServer.ps1, which becomes the following.

param (
  [string]$ServerToTest
)

function Get-NetworkInformation {
  param (
  [string]$server
  )

  $nic = Get-NetAdapter -Name LAN

  $ip = Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias LAN

  $dg = Get-NetIPConfiguration -InterfaceAlias LAN

  $props = [ordered]@{
    iIndex         = $nic.InterfaceIndex
    iAlias         = $nic.InterfaceAlias
    Status         = if ($nic.InterfaceOperationalStatus -eq 1){'Up'}else{'Down'}
    IPAddress      = $ip.IPAddress
    PrefixLength   = $ip.PrefixLength
    DefaultGateway = $dg.IPv4DefaultGateway | select -ExpandProperty NextHop
    DNSserver      = ($dg.DNSServer).serverAddresses
    Server         = $server
    ServerIP       = Resolve-DnsName -Name $server | select -ExpandProperty IPAddress
  }
  New-Object -TypeName PSobject -Property $props
}
$netinfo = Get-NetworkInformation -server $ServerToTest
$path = 'C:\Scripts\TroubleShooting\Tests\PingTests.ps1'

$tests = Get-Content -Path $path |
Select-String -Pattern 'Describe' |
foreach {
   (($_ -split 'Describe')[1]).Trim('{').Trim().Trim("'")
}

$data = foreach ($test in $tests) {
$result = $null
$result = Invoke-Pester -Script @{Path = $path} -PassThru -TestName "$test" -Show None

$props = [ordered]@{
    'Test' = $result.TestResult.Name
    'Result' = $result.TestResult.Result
    'Failure Message' = $result.TestResult.FailureMessage
  }
 New-Object -TypeName PSObject -Property $props

if ($result.FailedCount -gt 0) {break}
}

$data | Format-Table -AutoSize -Wrap

The script takes a parameter ServerToTest, which you use to input the server name. The function Get-NetworkInformation discovers the information required to run the tests using the Get-NetAdapter, Get-NetIPAddress and Get-NetIPConfiguration cmdlets. My network adapters have the name LAN to make access easier. You should name your adapters something easy for you to access, especially if you have multiple adapters in a system. Resolve-Dnsname gets the server's IP address from the name. The Get-NetworkInformation function populates the $netinfo variable.

The list of tests is automatically generated from the PingTests.ps1 file by using Get-Content to read the file and Select-String to find the lines with the Describe keyword.

An additional test checks if the DNS server is available, which also illustrates how $netinfo is used.

## test DNS server
Describe 'DNSServer' {
  It 'DNS server should be available' {
    Test-Connection -ComputerName $netinfo.DNSserver -Quiet -Count 1 |
    Should Be $true

  }
}

Run the script with the server name as a parameter.

.\Test-ServerPing.ps1 -ServerToTest W19FS01

The results are shown in Figure 2.

server ping test
Figure 2. When we run Test-ServerPing.ps1, the testing stops when a failure occurs.

You can add other tests, such as testing the default gateway.

The final versions of the code used for this article are available at this link. A further example of using Pester for troubleshooting -- this time for troubleshooting the configuration of PowerShell remoting -- is also available in the same repository. In the Test-Remoting.zip file, the RemotingTests.ps1 file contains the Pester tests, and the Test-Remoting.ps1 file contains the code to run the tests and stop when a problem is detected. The code assumes Windows PowerShell but can work in PowerShell Core with some modifications.

Why troubleshooting is still an important skill

Monitoring products, such as System Center Operations Manager, can report if a server or service goes offline and may remove the need for some of the troubleshooting scenarios, but there are many scenarios where this ability is still needed. Some of these include the following:

  • There isn't a monitoring tool in place.
  • The monitoring tool doesn't oversee the technology at the root of the problem.
  • Distributed environments have complicated networks that may not be monitored.
  • The issue is related to a configuration problem, which is outside the scope of any monitoring product.

If you adopt this troubleshooting approach, I recommend using the Test-X naming convention for the code that runs the tests. You could convert the Test-X scripts to a module that could utilize single copies of functions, such as Get-NetworkInformation, as hidden helper functions.

Next Steps

How to test PowerShell code with Pester

Dig Deeper on Microsoft messaging and collaboration