Search code examples
powershellunit-testingmockingautomated-testspester

Pester Unit Test - Mock Get-ADUser Not Working as Expected


First of all I'll apologise if this isn't the correct way to ask these questions, this is my first time submitting to SO.

The short version is :

I have a script Get-SamAccountName.ps1 that takes Input Variables of a -FirstName, -LastName, -Format to then generate a new SamAccountName based on the Format. Now the function itself works perfectly fine (tested against AD Objects to verify).

However when I am attempting to write some Pester Tests for this is where I am having some trouble, specifically around the Mock function of Pester.

The relevant part of the Function Get-SamAccountName is as a below (stripped of irrelevant items like Parameter Blocks etc.):

function Get-SamAccountName {
    param(
        [string[]]$FirstName,
        [string[]]$LastName,
        [string[]]$Format
    )

................

    # If SamAccountName already in use, add a number until you find a free SamAccountName to use.
    if (Get-ADUser -Filter {samaccountname -eq $BaseSam} -ErrorAction SilentlyContinue) {
        $index = 0
        do {
            $index++
            $SamAccountName = "{0}{1}" -f $BaseSam.ToLower(),$index
        } until (-not(Get-ADUser -Filter {samaccountname -eq $SamAccountName} -ErrorAction SilentlyContinue))
    } else {
        $SamAccountName = $BaseSam.tolower()
    }
    return $SamAccountName
}

This part of the function is where the Get-ADUser is used and what I am attempting to Mock within my Pester Test. Essentially a Base SamAccountName $BaseSam generated from the First and Last Names and then checked against AD to see if it exists or not, and if it does add a # to the end of the SamAccountName per iteration that exists.

My Pester Test file is as below, like above stripped of additional tests that do work (bar one as an example) :

$ModuleRoot = $env:BHModulePath
$ModuleName = $env:BHProjectName
$ModulePath = $env:BHPSModuleManifest
Remove-Module -Name $ModuleName -ErrorAction 'SilentlyContinue'
Import-Module $ModulePath -Force

InModuleScope $ModuleName {
    BeforeAll {
        # Load Public function
        . "$ModuleRoot\Public\Get-SamAccountName.ps1"

        function Get-ADUser ($Filter) {}
    }

    Describe -Tags ('Unit') "Unit Tests for Get-SamAccountName" {
        #Run tests based upon the Examples from the Function Docs
................

        Context -Tags ('Unit') "Using the -FLast Format" {
            It "Returns <SamAccountName> SamAccountName from FirstName <FirstName> and LastName <LastName> Inputs in the -FLast format." -TestCases @(
                #Test with string containing Diacritic characters.
                @{ FirstName = "Clärk"; LastName = "Ként"; SamAccountName = "ckent"}
                #Test with string containing Latin characters only.
                @{ FirstName = "Bruce"; LastName = "Wayne"; SamAccountName = "bwayne"}
            ) {
                Get-SamAccountName -FirstName $FirstName -LastName $LastName -Format 'FLast' | Should -Be $SamAccountName
            }
        }

................

        Context -Tags ('Unit') "A User Already Exists in Active Directory" {
            It "Returns <expected> SamAccountName from FirstName <FirstName> and LastName <LastName> with an increment number." -TestCases @(
                #Test with string containing Diacritic characters.
                @{ FirstName = "Clärk"; LastName = "Ként"; BaseSam = "ckent"; expected = "ckent1"}
                #Test with string containing Latin characters only.
                @{ FirstName = "Bruce"; LastName = "Wayne"; BaseSam = "bwayne"; expected = "bwayne"}
            ) {
                Mock Get-ADUser -MockWith { $BaseSam } -ParameterFilter { $Filter -eq {samaccountname -eq $BaseSam} }
                Get-SamAccountName -FirstName $FirstName -LastName $LastName -Format 'FLast' | Should -Be $expected
            }
        }
    }
}

Now the expected I intentionally have different at the moment as I wanted to see a test in which one fails and one passes to verify it has been set up correctly, with the end result having both set to username1 (the appended 1 to verify an existing AD User was discovered).

Running this technically works as above, with a pass/fail however it's the opposite of what I'm trying to achieve (Pester Output below) :

Describing Unit Tests for Get-SamAccountName
Context A User Already Exists in Active Directory
[-] Returns ckent1 SamAccountName from FirstName Clärk and LastName Ként with an increment number. 12ms (10ms|2ms)
    Expected strings to be the same, but they were different.
    Expected length: 9
    Actual length:   8
    Strings differ at index 8.
    Expected: 'ckent1'
    But was:  'ckent'
            --------^
    at Get-SamAccountName -FirstName $FirstName -LastName $LastName -Format 'FLast' | Should -Be $expected, /mnt/k/Repositories/CodeBlue.UserAutomation/Tests/Unit/Get-SamAccountName.Unit.Tests.ps1:70
    at <ScriptBlock>, /mnt/k/Repositories/CodeBlue.UserAutomation/Tests/Unit/Get-SamAccountName.Unit.Tests.ps1:70
[+] Returns bwayne SamAccountName from FirstName Bruce and LastName Wayne with an increment number. 9ms (8ms|1ms)
Tests completed in 281ms
Tests Passed: 1, Failed: 1, Skipped: 0 NotRun: 8

It's 100% likely it's either my syntax or the placement of items, I've only been using Pester for about a week now, and this is technically my first attempt at using a Mock. Have read countless threads here on SO as well as Blog Entries with examples, but can't for the life of me get this specific use case working as intended :(

I will note the above is just one iteration of Mock I have tried, but one of the only ones I've managed to get 'working' albeit in the opposite way of what I'm trying to do.

Any help at all would be so very much appreciated <3


Solution

  • This is quite tricky. It looks to me that there are two issues:

    1. Your Mock isn't firing because the -ParameterFilter isn't matching the invocation of Get-ADUser.
    2. You need your tests to Mock Get-ADUser in 2 different ways, so that when first used it returns a result but when invoked the 2nd time it returns nothing.

    I've created what I think is a working solution, but because getting -ParameterFilter to work well with Get-ADUser's -Filter parameter seems to be very difficult, I changed your function so that for one of the Get-ADUser calls it includes the -Properties parameter, so that I could differentiate the use based on that. An alternative approach might be to create two wrapper functions around Get-ADUser for when you use it the first vs 2nd time.

    Here's my solution. I also had to edit your function as I was missing the part where you set $BaseSam. This passes for Bruce Wayne but not for Clark Kent as it doesn't handle the foreign characters.

    BeforeAll {
        function Get-SamAccountName {
            param(
                [string]$FirstName,
                [string[]]$LastName,
                [string[]]$Format
            )
        
            $BaseSam = $FirstName[0] + $LastName
    
            # If SamAccountName already in use, add a number until you find a free SamAccountName to use.
            if (Get-ADUser -Filter { samaccountname -eq $BaseSam } -Properties SamAccountName -ErrorAction SilentlyContinue) {
                $index = 0
                do {
                    $index++
                    $SamAccountName = "{0}{1}" -f $BaseSam.ToLower(), $index
                } until (-not(Get-ADUser -Filter { samaccountname -eq $SamAccountName } -ErrorAction SilentlyContinue))
            }
            else {
                $SamAccountName = $BaseSam.tolower()
            }
            return $SamAccountName
        }
    
        function Get-ADUser {
            param( $Filter, $Properties)
        }
    }
    
    Describe -Tags ('Unit') "Unit Tests for Get-SamAccountName" {
    
        Context -Tags ('Unit') "A User Already Exists in Active Directory" {
    
            It "Returns <expected> SamAccountName from FirstName <FirstName> and LastName <LastName> with an increment number." -TestCases @(
                #Test with string containing Diacritic characters.
                @{ FirstName = "Clärk"; LastName = "Ként"; BaseSam = "ckent"; expected = "ckent1" }
                #Test with string containing Latin characters only.
                @{ FirstName = "Bruce"; LastName = "Wayne"; BaseSam = "bwayne"; expected = "bwayne1" }
            ) {
                Mock Get-ADUser -MockWith { $BaseSam } -ParameterFilter { $Properties -eq 'SamAccountName' }
                Mock Get-ADUser -MockWith { }
    
                Get-SamAccountName -FirstName $FirstName -LastName $LastName -BaseSam $BaseSam -Format 'FLast' | Should -Be $expected
            }
        }
    }
    

    Note my example has your function in the BeforeAll just for ease of testing, you can continue just importing your module.