question around how to unit test with pester a powershell function that gathers

This topic contains 11 replies, has 2 voices, and was last updated by  Nicholas Herbert 3 months, 2 weeks ago.

  • Author
    Posts
  • #93042

    Nicholas Herbert
    Participant

    Hi there

    Im a newcomer to the Pester testing framework for powershell and have a question about how to unit test a powershell function I have written that gathers errors or warnings from system or security logs – this is because it is a requirement when submitting this into source control – I have written (what I think is an acceptance test in pester for the script) but unsure how one would go about writing a unit test for this – the code is not really changing anything in the environment only pulling error entries from 1 or multiple servers and outputting in out-gridview. The pester test I have written – tests 1) that the properties returned are correct and 2) that 50 rows of data are returned and 3) tests with both parameter and pipeline input – but im not sure how you would unit test this as from my reading on the subject (and limited knowledge) unit tests are supposed to be isolated and not make actual live calls to a server or service outside the unit test – here is the actual script – and the pester test I have written so far: any help on this would be appreciated

    function Get-EventData
    {
    
        [CmdletBinding()]
    
        param(
        [Parameter(
        Mandatory=$true,
        ValueFromPipeline = $true
    
        )]
        [String[]]$targetmachine,
    
    
        [parameter( Mandatory=$true, Position=1)]
        [ValidateSet("Application","System")]
        [string]$log,
    
        [parameter( Mandatory=$true, Position=2)]
        [ValidateSet("Error","Warning")]
        [string]$eventtype,
    
        [parameter( Mandatory=$true, Position=3)]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$credential
    
        )
    
    
            begin{
    
    $machinelogs=@()
    
        }
    
        process{
    
    
       foreach($target in $targetmachine)
       {
    
               try{
    
                        $evt = Invoke-Command -ComputerName $target `
                        -ScriptBlock{
    
                            Get-EventLog -LogName $using:log -EntryType $using:eventtype -Newest 50
    
                } -Credential $credential -ErrorAction Stop
                }catch{Write-Host -ForegroundColor Green "Failed to connect to remote server $target";continue}
    
    
                $evt | % {
    
                     $obj = New-Object psobject
                     $obj | Add-Member NoteProperty MachineName $_.pscomputername
                     $obj | Add-Member NoteProperty TimeGenerated $_.timegenerated
                     $obj | Add-Member NoteProperty EventID $_.EventID
                     $obj | Add-Member noteproperty EntryType $_.entrytype
                     $obj | Add-Member NoteProperty Source $_.source
                     $obj | Add-Member NoteProperty Message $_.message
                     $machinelogs += $obj
                    }
    
                $evt = $null
        }
    
        }  #endprocess
    
       end{
    
                $machinelogs | Out-GridView -Title "Machine Events" -PassThru
        }
    
    }
    

    and here is the pester test

    # Dot Source the function to load into memory
    . $PSScriptRoot\Get-EventData.ps1
    
    Describe 'Get-EventData Acceptance Tests' -Tags 'Acceptance' {
    
        $calls = 'parameter','pipeline'    # user to validate use of parameter or pipeline call to function
        foreach($call in $calls)
        {
                if($call -eq 'parameter')
                {
                    $eventdata = Get-EventData -targetmachine exchangeserver -log Application -eventtype Error -credential admin    
                }
                else
                {
                    $eventdata = 'exchangeserver' | Get-EventData -log Application -eventtype Error -credential admin
                }
    
        
                $rowNum = 0
                foreach($event in $eventdata)
                {
                    $rowNum++
                    Context "Event $rowNum has the correct properties" {
    
                        #Load array with properties that should be present
                        $properties = ('MachineName','TimeGenerated','EventID', 'EntryType',
                                        'Source', 'Message')
    
                        foreach($property in $properties)
                        {
                            It "EventData $rownum should have a property of $property" {
    
                            [bool]($event.PSObject.Properties.Name -match $property) |
                                Should Be $true
                        }
                  } #foreach
    
                    } #end context block
    
                } # foreach ($event in $eventdata)
    
                Context " 50 rows should be returned per Server" {  # context block to validate 50 rows returned
                    It "Count Should be equal to 50" {
                
                        $eventdata.count | Should Be 50
    
                    }
    
                }# end context block
        } # end calls foreach loop
    $rowNum = 0
    }#end describe block
    
  • #93052

    nohwnd
    Participant

    The best way to unit test this is to record data from some real server and pass them through your function (using a Mock of Get-EventLog), that way you can test the receiving some data you are processing them correctly. You can also test that when you get exception you handle it correctly.

    The last piece of puzzle is the GridView, and that would extract to a different function because I don't like imposing how the data will be viewed on the user, and it will also be simpler to test when you have function that outputs object, and function that takes those objects and pipes them into out-gv.

  • #93054

    Nicholas Herbert
    Participant

    Thanks for that nohwnd,

    I sorta get what your saying alright but when you say Mock of Get-EventLog do you mean actually run the real Get-EventData function against a server and export it to XML via export-clixml and then use import-clixml to use as mocked data? This is where i get confused – the acceptance test is more straightforward because your testing the real function....

    Not sure to be honest how to test for exceptions and handling – I added -passthru parameter to Out-GridView so that it would pass objects in the $eventdata variable so I could test against it – could you provide a code sample from the above code I've written to clarify please?

  • #93057

    nohwnd
    Participant

    Yes, that is what I mean. The goal of your unit test is that you can process data correctly. The more real-life-like the data are the bigger the chance is that your code will work against real servers. Unit tests (especially in PowerShell which is all about integrating with the real world) are compromise between ease of setup, and fidelity (and speed but disregard that).

    Unit tests are easier to setup because they don't require real infrastructure to be in the correct state, but at the cost of less fidelity, so if the format of the data changes on the real server, your unit test will not know.

    Errors: Personally I would just let the errors bubble through, when you don't change the default behavior of the cmdlet you don't have to test it.

    • #93060

      Nicholas Herbert
      Participant

      Hi there

      yes I understand the concept of what your saying – now have to figure out how to implement that –
      I mean (as i said) i could just run Get-EventData -targetserver 'bogusserver' -log application -eventtype error -credential admin and pipe it Export-CliXml $events – then run $eventinfo = Import-CliXml $events and just repeat the context and It blocks from the existing tests ive already written but is that a real unit test? seems redundant to me – if when you say 'Mock of Get-EventLog' bearing in mind its being called inside a 'Invoke-Command' cmdlet – do you mean rewriting the existing Get-EventData function for the unit test? Sorry am pretty new to this

    • #93096

      nohwnd
      Participant

      Missed that you are calling it in the Invoke-Command, the spacing betrayed me 🙂 In that case please mock Invoke-Command.
      So in this we would be testing that we are processing the data correctly. The source of the data is the Invoke-Command, so we do

      Mock Invoke-Command { Get-CliXml "EventLogData.clixml" }

      Rest of your test.

    • #93213

      Nicholas Herbert
      Participant

      Hi there

      maybe im being dumb but im trying this for the unit test but it errors for me – and gives me an error of:
      [-] Error occurred in Context block 93ms
      RuntimeException: You did not declare a mock of the Invoke-Command Command.
      I pasted the code i've tried so far for this – as i said the acceptance test is fine for me cos I can call the real function ive written and test it with correct properties returned, no of rows and using either parameter or pipeline – but the unit test im finding harder to wrap my head around – i captured live data from a server and put it into an XML file to use – but I still have to call the 'real function' within the Unit test surely? except its replacing the real Invoke-Command with the mocked one and returning the dummy/mock data and performing same set of tests again? I don't know if its a scoping issue or something else – because there are multiple context blocks within the foreach loop created and then another context block for the Assert-MockCalled just to verify that the mocked Invoke-Command is called? I tried mocking Get-EventLog itself which is used inside the Invoke-Command but this didn't work possibly a limitation around using a remote command inside Invoke-Command? not sure – I did take the advice on removing the out-gridview – realised that was unnessary and the end user can format the data whatever way they like – Thanks

      Describe 'Get-EventData Unit Tests' -Tags 'Unit' {
          
           
           
          
                      Mock Get-EventLog {
                          $MockEventData = Import-Clixml "C:\Users\desktop\eventdata.xml"
                          $eventdata = [System.Management.Automation.PSSerializer]::DeserializeAsList($mockEventData)
                          return $eventdata
      
      
                      }
      
                  $eventdata = Get-EventData -targetmachine server -log Application -eventtype Error -credential $credentials
                  $rowNum = 0
                  foreach($event in $eventdata)
                  {
                      $rowNum++
                      Context "Event $rowNum has the correct properties" {
      
                          #Load array with properties that should be present
                          $properties = ('MachineName','TimeGenerated','EventID', 'EntryType',
                                          'Source', 'Message')
      
                          foreach($property in $properties)
                          {
                                  It "EventData $rownum should have a property of $property" {
      
                                  [bool]($event.PSObject.Properties.Name -match $property) |
                                      Should Be $true
                              }
                          } #foreach
      
                      } #end context block
      
                  } # foreach ($event in $eventdata)
      
                  Context " 50 rows should be returned per Server" {  # context block to validate 50 rows returned
                      It "Count Should be equal to 50" {
                  
                          $eventdata.count | Should Be 50
      
                      }
      
                  }# end context block
      
      
                  Context " Should have called Mocked Invoke-Command " {
                      Assert-MockCalled Invoke-Command
                  }
        
      $rowNum = 0
      }#end describe block
      
    • #93244

      nohwnd
      Participant

      Line 6. You are mocking get-eventlog not invoke-command. 🙂

    • #93247

      Nicholas Herbert
      Participant

      Hi there

      sorry typo – there but even with Mock Invoke-Command and exact same code in the script block I get:

      Context Should have called Mocked Invoke-Command
      [-] Error occurred in Context block 69ms
      Expected Invoke-Command to be called at least 1 times but was called 0 times

    • #93250

      nohwnd
      Participant

      hard for me to say, try setting a breakpoint on invoke-command in your function to see if you reach it or not.

    • #93252

      Nicholas Herbert
      Participant

      Hi there

      set a breakpoint on the Invoke-Command in the 'real' function and it shows
      Stopped at: dynamicparam { Get-MockDynamicParameter -CmdletName 'Invoke-Command' -Parameters $PSBoundParameters }

      it then jumps to Mock.ps1 and does a bunch of stuff over code i dont understand
      but eventually hits the catch block in the Get-EventData.ps1 with failed to connect to server

  • #93261

    Nicholas Herbert
    Participant

    Hi there
    got it to run successfully – and thanks for your help added -scope Describe to the end of the Assert-MockCalled cmdlet
    interesting thing i noticed – I ran the script with a breakpoint on the
    $eventdata = [System.Management.Automation.PSSerializer]::DeserializeAsList($mockEventData) line
    but the debugger never entered into it – its as if its running in its own runspace but i could verify that the data returned by the Get-EventData function was the mockeddata – (looking at the dates of the event logs) – here is the unit test i came up with
    unit testing certain powershell functions can be trickier than the actual function that was written – ie. trying to figure out what exactly to test 🙂 I've pasted in the code i wrote below – (changing any PII data) – feel free to give your feedback or appraisal
    Thanks

    Param (
        [Parameter(Mandatory=$false)]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$credential
        
    )
    
    
    # Dot Source the function to load into memory
    . $PSScriptRoot\Get-EventData.ps1
    
    
    
    Describe 'Get-EventData Unit Tests' -Tags 'Unit' {
        
        $MockEventData = @'
        
    
    
      
        
          System.Management.Automation.PSCustomObject
    	blahblah
    
    '@  
         
        
                    Mock Invoke-Command {
                        $eventdata = [System.Management.Automation.PSSerializer]::DeserializeAsList($mockEventData)
                        return $eventdata
    
    
                    }
        $calls = 'parameter','pipeline'    # user to validate use of parameter or pipeline call to function
        foreach($call in $calls)
        {
                if($call -eq 'parameter')
                {
                    $eventinfo = Get-EventData -targetmachine exchangeserver -log Application -eventtype Error -credential $credential  
                }
                else
                {
                    $eventinfo = 'exchangeserver' | Get-EventData -log Application -eventtype Error -credential $credential
                }
               
                $rowNum = 0
                foreach($event in $eventinfo)
                {
                    $rowNum++
                    Context "Event $rowNum has the correct properties" {
    
                        #Load array with properties that should be present
                        $properties = ('MachineName','TimeGenerated','EventID', 'EntryType',
                                        'Source', 'Message')
    
                        foreach($property in $properties)
                        {
                                It "EventData $rownum should have a property of $property" {
    
                                [bool]($event.PSObject.Properties.Name -match $property) |
                                    Should Be $true
                            }
                        } #foreach
    
                    } #end context block
    
                } # foreach ($event in $eventinfo)
        
                Context " 50 rows should be returned per Server" {  # context block to validate 50 rows returned
                    It "Count Should be equal to 50" {
                
                        $eventinfo.count | Should Be 50
    
                    }
    
                }# end context block
    
    
                Context " Should have called Mocked Invoke-Command " {
                    Assert-MockCalled Invoke-Command -Scope Describe
                }
      }
    $rowNum = 0
    }#end describe block
    

You must be logged in to reply to this topic.