Seeking Guidance on Mocking Azure Commands in Pester Tests for PowerShell Class

Hi everyone,

I’m running into a recurring issue when running Pester tests on a PowerShell class that interacts with Azure resources, specifically with Get-AzResource. The tests fail with an error indicating a need to login via Connect-AzAccount.

Key Points:

  • The class works as intended outside of Pester, making successful Azure API calls.
  • The issue arises during Pester tests that involve methods fetching diagnostic settings from Azure, where there is no Azure context already configures.

Error Received Across Multiple Tests:

PSInvalidOperationException: Run Connect-AzAccount to login.

What I’ve done:
The structure of my Powershell Class (simplified):

using namespace System.Collections
class ResourceLogCategory {
    [string] $ResourceTypeName
    [string] $ResourceGroupName
    [array] $LogCategory = @()
    [array] $MetricCategory = @()

    ResourceLogCategory([string]$ResourceGroupName, [string]$ResourceTypeName) {
        $this.ResourceGroupName = $ResourceGroupName
        $this.ResourceTypeName = $ResourceTypeName
        $this.FetchDiagnosticSettings()
    }

    [void] FetchDiagnosticSettings() {
        try {
            $resource = Get-AzResource -ResourceGroupName $this.ResourceGroupName -ResourceType $this.ResourceTypeName | Select-Object -First 1
            $diagnosticSettings = Get-AzDiagnosticSetting -ResourceId $resource.ResourceId
            
            foreach ($setting in $diagnosticSettings) {
                if ($setting.Logs) {
                    $this.LogCategory += $setting.Logs.Id
                }
                if ($setting.Metrics) {
                    $this.MetricCategory += $setting.Metrics.Id
                }
            }
        } catch {
            Write-Error "Failed to fetch diagnostic settings."
        }
    }
}

My pester test:

# Suppressing this rule because Script Analyzer does not understand Pester's syntax.
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
param ()

$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path
$ProjectName = (Get-ChildItem $ProjectPath\*\*.psd1 | Where-Object {
        ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and
        $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop }catch { $false }) }
).BaseName

# Import the project module
Import-Module $ProjectName

# Define tests within the scope of the project module
InModuleScope $ProjectName {

    BeforeAll {

        # Define properties for valid and invalid test objects
        $ValidObj1Properties = @{
            SourceType       = "Az"
            ContainerId      = "11111111-1111-1111-1111-111111111111"
            ResourceTypeName = "Provider.Resource1/Type"
        }
        $ValidObj2Properties = @{
            SourceType       = "Az"
            ContainerId      = "22222222-2222-2222-2222-222222222222"
            ResourceTypeName = "Provider.Resource2/Type"
        }
        $InvalidObjProperties = @{
            SourceType       = "Az"
            ContainerId      = "ObjectId"
            ResourceTypeName = "Provider.Resource3/Type"
        }
        $IncompleteObjProperties = @{
            SourceType  = "Az"
            ContainerId = "00000000-0000-0000-0000-000000000000"
        }

        # Mock the Get-AzDiagnosticSettingCategory function to return predefined results for testing
        Mock -CommandName 'Get-AzDiagnosticSettingCategory' -MockWith {
            param($ResourceId)

            if ($ResourceId -eq "11111111-1111-1111-1111-111111111111") {
                return @(
                    @{ Name = "LogCategory1"; CategoryType = "Logs" },
                    @{ Name = "MetricCategory1"; CategoryType = "Metrics" }
                )
            }
            else {
                return @()
            }
        } -Verifiable

        # Mock the Get-AzResource function to return predefined results for testing
        Mock -CommandName 'Get-AzResource' -MockWith {
            param($ResourceId)

            if ($ResourceId -eq "11111111-1111-1111-1111-111111111111") {
                return @{ ResourceType = "Provider.Resource1/Type" }
            }
            elseif ($ResourceId -eq "22222222-2222-2222-2222-222222222222") {
                return @{ ResourceType = "Provider.Resource2/Type" }
            }
            else {
                return @{ ResourceType = "Provider.Resource3/Type" }
            }
        } -Verifiable
    }
    # Describe the group of tests for the ResourceLogCategory class
    Describe 'ResourceLogCategory Class Unit Tests' {
        # Define setup actions to be performed before all tests


        # Describe the group of tests for property initialization
        Describe 'ResourceLogCategory Class Property Initialization' {
            # Test that valid data input is validated correctly
            It 'should validate correct data input' {
                { [ResourceLogCategory]::Validate(
                        $ValidObj1Properties.ContainerId,
                        $ValidObj1Properties.ResourceTypeName,
                        $ValidObj1Properties.SourceType) } | Should -Not -Throw
            }
            # Test that invalid data input is not validated
            It 'should not validate incorrect data input' {
                { [ResourceLogCategory]::Validate(
                        $InvalidObjProperties.ContainerId,
                        $InvalidObjProperties.ResourceTypeName,
                        $InvalidObjProperties.SourceType) } | Should -Throw
            }
            # Test that empty data input is not validated
            It 'should not validate empty data input' {
                { [ResourceLogCategory]::Validate('', '', '') } | Should -Throw
            }
        }

        # Describe the group of tests for constructors
        Describe 'ResourceLogCategory Class Constructors Tests' {
            # Test that the object is created correctly with separate properties
            It 'Should create the object with separate properties' {
                { [ResourceLogCategory]::new(
                        $ValidObj1Properties.ContainerId,
                        $ValidObj1Properties.ResourceTypeName,
                        $ValidObj1Properties.SourceType) } | Should -Not -Throw
            }
            # Test that the object is created correctly from a hashtable
            It 'should create an object with constructor from hashtable' {
                { $obj = [ResourceLogCategory]::new($ValidObj1Properties) } | Should -Not -Throw
            }
        }

        # Describe the group of tests for methods
        Describe 'ResourceLogCategory Class Methods Tests' {

            # Test that the method handles resource types with diagnostic settings available
            It 'should handle resource type with diagnostic settings available' {
                $obj = [ResourceLogCategory]::new($ValidObj1Properties)
                { $obj.GetDiagnosticSettings() } | Should -Not -Throw
            }

            # Test that the method handles resource types with no diagnostic settings
            It 'should handle resource type with no diagnostic settings' {
                $obj = [ResourceLogCategory]::new($ValidObj2Properties)
                { $obj.GetDiagnosticSettings() } | Should -Not -Throw
            }

            # Test that the method returns a string representation of the object
            It 'should return a string representation of the object' {
                $obj = [ResourceLogCategory]::new($ValidObj1Properties)
                { $obj.ToString() } | Should -Not -Throw
            }
        }

    }
}

Errors Details:

[-] should create an object with constructor from hashtable 32ms (29ms|2ms)

[215](https://github.com/CyberShell-app/CyberShell/actions/runs/8466801601/job/23196375390#step:4:217)Message

[216](https://github.com/CyberShell-app/CyberShell/actions/runs/8466801601/job/23196375390#step:4:218) Expected no exception to be thrown, but an exception "Run Connect-AzAccount to login." was thrown from /home/runner/work/CyberShell/CyberShell/output/module/CyberShell/0.1.0/CyberShell.psm1:88 char:21

[217](https://github.com/CyberShell-app/CyberShell/actions/runs/8466801601/job/23196375390#step:4:219) + … $resource = Get-AzResource -ResourceType $this.ResourceTypeName | Sel …

[218](https://github.com/CyberShell-app/CyberShell/actions/runs/8466801601/job/23196375390#step:4:220) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.

[219](https://github.com/CyberShell-app/CyberShell/actions/runs/8466801601/job/23196375390#step:4:221) at { $obj = [ResourceLogCategory]::new($ValidObj1Properties) } | Should -Not -Throw, /home/runner/work/CyberShell/CyberShell/tests/Unit/Class/ResourceLogCategory.tests.ps1:108

Any ideas on how to solve this issue ?

I haven’t really used Pester but I do have a couple thoughts:

  1. That’s the exact same error you’d get if you were in a powershell window and hadn’t connected or had a current AZ Context. You can confirm by disconnecting, removing your contexts, and trying to run Set-AZResource. That makes me feel like it’s not actually using your mocked command, and instead using the real command. Have you tried to confirm this?
  2. Doing some digging on 1, I came across an interesting note in the official docs Mocking with Pester | Pester -Mocking a function that is called by a method in a PowerShell Class talks about mocking a function that is called by a class. That sort of seems like what you are doing. If you happen to be using 5.1 (or earlier) it seems like you may need to do a work around using start-job (which I presume just makes it so the class definitions aren’t cached).

Yes and I confirm this is the same behaviour when not connected to an AzContext

Good catch; but nor sure this is applicable to me as I am running powershell core (v7).
And if it would be applicable, it means I would need to use a start-job to run the Connect-AzContext, and it could be tricky to securely manage the credentials.

Yea not sure… Definitely seems like it’s calling the actual cmdlet and not your mocked one, so I assume if you can figure out why it’s doing that you’ll solve the issue. My gut feeling is it’s still related to that cache/memory thing when it loads that class into memory its referencing the real cmdlet.

I’m not sure you’d need to use Start-Job to run a Connect command. FWIW There is no Connect-AZContext, but think you mean Connect-AZAccount. It seems like the idea was it was reloading the stuff, and therefore would reference your mocked command instead on the new load. It seems like one of the obvious benefits of mocking that Set-AZResource, is the fact that you don’t need to make a real API call or have any of the requirements that would entail as part of the test, which can be challenging, and likely not that helpful, as you’re trying to confirm your commands work not the commands they use, so you just mock a command and force an output so you can properly test that your command behaves properly based on your output(s).

yes i was refering to this Cmdlet

yes indeed, but this is where this is unclear on how we should proceed