Skip to main content

PowerShell + AD + Pester : create new user using template Part 2

It seems like it has taken me forever to post this one. I had this one almost ready but then I asked few questions around, read a lot of posts and had to rewrite the pieces of the post, to sum it all it has been an eye opening when trying to test PowerShell code which interacts with Infrastructure.

Below pic depicts my state at this point ( revelation to a whole new world).


[ credits : movie "V for Vendetta"]

In the last post, we laid the foundation for our Function. Go back and check the code their as we start from where we left off.

In this post we dive straight into the third context for our Pester tests :
  1. Context "User Creation"
      It should return Object when -Passthru specified (New addition)
      It should take OU Path from template User.
      It should only copy allowed set of attributes from the User (by default).
      It should allow copying a subset of allowed set of attributes
    .
      
Note - I have added one more test to the context, which is in green. Why rest of the functions are marked in Red :O ? Answer to this follows in the conclusion section.


Before writing the tests, I wanted to share one important concept while practicing TDD/BDD. 

If at any point it becomes a pain writing tests then you are probably doing it wrong. Tests should be easy to write otherwise they depict a serious flaw in the logic you have. [Lesson learned hard way]

Also, one has to remember that Unit tests only test the logic of the code. So if I wrote code to create an AD user, my unit tests shouldn't be creating a test user each time (this would clutter things up). Easier way to test the logic is to mock the key pieces which have external dependency.

But Mocking is a slippery slope, when we use mocking we are actually testing the behavior of the code rather than the state that is why this approach is also termed as "Behavior" based testing.



Setting up Context


A quick search found me below link on Technet (going to use this as reference for my function and will keep refactoring this).
https://technet.microsoft.com/en-us/library/dd378959(v=ws.10).aspx

So it is clear, New-ADUser is the cmdlet (part of the AD PowerShell module) which will be ultimately called for creating user in function.

In my above example, querying AD is an external dependency it has nothing to do with the logic for my code. So it is wise to mock those cmdlets which will have sort of external dependency.


It is obvious that in order to create a new user I would be using New-ADUser cmdlet from the AD PowerShell module, but I will have to mock it and assert that it is being called by my code. So the context for my "user creation" tests will look like below to begin with :


001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
    Context "User Creation" {
 
        BeforeEach {
            # Create a Dummy AD module and import it
            $DummyModule = New-Module -Name ActiveDirectory  -Function "New-ADUser","Get-ADUser" -ScriptBlock {
                                                                            Function New-ADUser {"New-ADUser"} ;
                                                                        }
            $DummyModule| Import-Module
        }

        AfterEach {
            # Forcefully remove the Dummy AD Module
            Remove-Module -Name ActiveDirectory -Force  -ErrorAction SilentlyContinue
        }
 
    }



Before moving forward take a look at the Context block one more time. The BeforeEach{} and AfterEach{} block provide a way to setup and teardown test environment for each Unit test run (the It{} blocks).
Note - If you haven't heard about the BeforeEach {} block earlier then take pause and read Michael Sorens article on Test anatomy.

So in the BeforeEach{} block, I create a dummy module named ActiveDirectory (AD PowerShell module which is not present in my local machine) and export Get-ADUser, New-ADUser functions.

In the AfterEach{} block, the Remove-Module is called to unload the dummy module forcefully. Because of the above hack, I had to modify the Module loading part in my code(see the try block below which got changed). 
Now the code tests if the module is already loaded because the dummy module is loaded in Beforeach{} block so the function should see it and do not try to load the module again.


001
002
003
004
005
006
007
        TRY {
            if ( -not (Get-Module -Name ActiveDirectory) ) {
                # try to import the Module
                Import-Module -name ActiveDirectory -ErrorAction stop
                $null = Get-PSDrive -Name AD -ErrorAction stop  # Query if the AD PSdrive is loaded
            }
        }

The reason to use the dummy module are :
  1. Pester tests don't depend on AD PowerShell module.
  2. AD PowerShell module is too complicated in implementation, Read more here

Test - It should return Object when -Passthru specified (New addition)


Now let's take a look at the test.

001
002
003
004
005
006
007
008
It "Should return object when -Passthru specified" {
            $TemplateUser = [PSCustomObject]@{Name='templateuser';UserPrincipalName='templateuser@dex.com'}
            Mock -CommandName New-ADuser  -MockWith {@{name='testuser'}} -Verifiable
            $CreatedUser = New-ADUserFromTemplate -GivenName 'test 123' -SamAccountName 'test123' -Instance $TemplateUser -Passthru
            Assert-VerifiableMocks # Assert that our verifiable mock for New-ADuser cmdlet was called.
            $Createduser | Should Not BeNullOrEmpty
     
        }

Inside the Unit test, I begin by storing a custom object in $TemplateUser which will be passed to our function.
Then the New-ADUser function (Yes! it is a function loaded from my dummy AD module) is mocked to return a hashtable (it could be a custom object too). Observant eye will notice that the mock has been marked -verifiable.


Later the Assert-VerifiableMocks is called to verify that all the verifiable mocks have been invoked. Also at the end I assert that the $CreatedUser should not be empty (-Passthru should return Object).

The tests fail at this point (Red phase) because I didn't modify the code, so I change my function definition.

So my bare bone code looks like below now (removed help for this one only here in this illustration not from the actual code). People will criticize me for passing the -Name an argument same as SamAccountName but that is fine, more focus is on the testing philosophy here.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
function New-ADUserFromTemplate {

   param(
        # SPecify the unique SamAccountName for the User
        [Parameter(Mandatory)]               
        [ValidateNotNullOrEmpty()]
        [string]$SamAccountName,
   
        # Specify the First Name or Given name for the user
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String]$GivenName,

        [Parameter(Mandatory)]
        #[PSTyepName('Microsoft.ActiveDirectroy.Management.ADUser')]
        [Object]$Instance,

        [Switch]$Passthru
        )

        TRY {
            if ( -not (Get-Module -Name ActiveDirectory) ) {
                # try to import the Module
                Import-Module -name ActiveDirectory -ErrorAction stop
                $null = Get-PSDrive -Name AD -ErrorAction stop  # Query if the AD PSdrive is loaded
            }
        }
        CATCH [System.IO.FileNotFoundException]{
            Write-Warning -Message $_.exception
            throw "AD module not found"
        }
        CATCH {
            throw $_.exception
        }         

        # Let's start by following the link : https://technet.microsoft.com/en-us/library/dd378959(v=ws.10).aspx
        $Instance.UserPrincipalName = $Null

        if ($Passthru.IsPresent) {   
            New-ADuser -Name $SamAccountName -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False -Passthru
        }
        else {
          $null =  New-ADuser -Name $SamAccountname -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False
        }  
}

Now if I run the test then it should pass (Green phase) this particular unit test.

Test - It should NOT return object (by default)


Notice how I have added two more tests as I get clarity on the behavior of the function.
This unit test will check the exact opposite behavior of the previous test.



001
002
003
004
005
006
007
It "Should NOT return object by default" {
            $TemplateUser = [PSCustomObject]@{Name='templateuser';UserPrincipalName='templateuser@dex.com'}
            Mock -CommandName New-ADuser  -MockWith {@{name='testuser'}} -Verifiable
            $CreatedUser = New-ADUserFromTemplate -GivenName 'test 123' -SamAccountName 'test123' -Instance $TemplateUser
            Assert-VerifiableMocks # Assert that our verifiable mock for New-ADuser cmdlet was called.
            $Createduser | Should  BeNullOrEmpty
        }

The key difference in this unit test is that $Createduser should be empty. 

When I run the Pester test at this point the second unit test fail, but wait in the function definition New-ADUser cmdlet is not passed -Passthru switch if it is not specified to the New-ADUserFromTemplate function.

See below excerpt from the function, it should have taken care of the test (right?)


001
002
003
004
005
006
        if ($Passthru.IsPresent) {  
            New-ADuser -Name $SamAccountName -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False -Passthru
        }
        else {
            New-ADuser -Name $SamAccountname -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False
        }

This is the downside of using a dummy dynamic module the BeforeEach{} trick, because the code just sees a New-ADUser dummy function, it doesn't mimic the behavior of the cmdlet. This approach comes with this downside but this shouldn't be hard to fix.

If I change the above code excerpt (if else condition) like below :



001
002
003
004
005
006
007
        if ($Passthru.IsPresent) {  
            New-ADuser -Name $SamAccountName -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False -Passthru
        }
        else {
          $null =  New-ADuser -Name $SamAccountname -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False
        }      

Now all the tests are passing, below is a screenshot showing that. 


Conclusion

We are at the end of this post, few more posts will follow. Wait ! What ?
No more tests (marked in red below) as mentioned in the starting of the post for the context block !!!

Context "User Creation"
  It should take OU Path from template User.
  It should only copy allowed set of attributes from the User (by default).
  It should allow copying a subset of allowed set of attributes.
Honestly, I had all those unit tests written but later I realized that the tests are actually checking the state of the Created user rather than the behavior of the function. If you have looked carefully the object returned from my function New-ADUserFromTemplate in the current context is a mocked object ( it is in my control, what I wish to return).

So ask yourself does it make sense to run tests which check the state of a mocked object ? You can only run these tests when you get an actual AD User which was created in AD. 

To summarize these tests are testing the "State" of the Object hence we can't use mocking here. This completely changes the initial strategy I had in my mind for testing my code for good.

Please read this article by Matt Wrock as it perfectly describes the dilemma I faced while writing this post, especially the part where he talks about mocking infrastructure.

http://www.hurryupandwait.io/blog/why-tdd-for-powershell-or-why-pester-or-why-unit-test-scripting-language

If you have anything to add on to my current approach then the feedback is much appreciated.

Below is the final state my code has achieved

Full Test Suite :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")

. "$here\$sut"

#region Unit Test - Test only the logic, Mock the Shit out of Variables !
Describe "New-ADUserFromTemplate" -Tags 'UnitTest'{


    Context "Help and Parameter checks" {
        Set-StrictMode -Version latest
  
        It 'should have inbuilt help along with Description and examples' {
            $helpinfo = Get-Help New-ADUserFromTemplate
            $helpinfo.examples | should not BeNullOrEmpty  # should have examples
            $helpinfo.Details | Should not BeNullOrEmpty   # Should have Details in the Help
            $helpinfo.Description | Should not BeNullOrEmpty # Should have a Descriptiong for the Function
        }

        It 'Should have SamAccountName, GivenName & Instance Mandatory params' {
            # {New-ADuserFromTemplate} | Should Throw
            {New-ADuserFromTemplate -samAccountName $null } | should throw
            {New-ADuserFromTemplate -GivenName $null} | should throw
            {New-ADuserFromTemplate -Instance $null } | should throw
            {New-ADuserFromTemplate -GivenName $Null -SamAccountName $null -Instance $Null } | Should Throw
        }
    } # end Context

    Context "ActiveDirectory Module Available" {
        $TemplateUser = [pscustomobject]@{
                                            Name='testuser'
                                            UserPrincipalName='testuser@dex.com'
                                            #PStypeName = 'Microsoft.ActiveDirectory.Management.ADUser'
                                            }


        It "Should Fail if the AD Module not present" {
            Mock Get-Module -MockWith {$Null}
            Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable
            {New-ADUserFromTemplate -SamAccountName test123 -GivenName 'test 123' -Instance $TemplateUser } | should throw    
            Assert-VerifiableMocks
        }
    }

    Context "User Creation" {
  
        BeforeEach {
            # Create a Dummy AD module and import it
            $DummyModule = New-Module -Name ActiveDirectory  -Function "New-ADUser","Get-ADUser" -ScriptBlock {
                                                                            Function New-ADUser {"New-ADUser"} ;
                                                                        }
            $DummyModule| Import-Module
        }

        AfterEach {
            # Forcefully remove the Dummy AD Module
            Remove-Module -Name ActiveDirectory -Force  -ErrorAction SilentlyContinue
        }
  
  
        It "Should return object when -Passthru specified" {
            $TemplateUser = [PSCustomObject]@{Name='templateuser';UserPrincipalName='templateuser@dex.com'}
            Mock -CommandName New-ADuser  -MockWith {@{name='testuser'}}  -Verifiable
            $CreatedUser = New-ADUserFromTemplate -GivenName 'test 123' -SamAccountName 'test123' -Instance $TemplateUser -Passthru
            Assert-VerifiableMocks # Assert that our verifiable mock for New-ADuser cmdlet was called.
            $Createduser | Should Not BeNullOrEmpty
      
        }

        It "Should NOT return object by default" {
            $TemplateUser = [PSCustomObject]@{Name='templateuser';UserPrincipalName='templateuser@dex.com'}
            Mock -CommandName New-ADuser  -MockWith {@{name='testuser'}} -Verifiable
            $CreatedUser = New-ADUserFromTemplate -GivenName 'test 123' -SamAccountName 'test123' -Instance $TemplateUser
            Assert-VerifiableMocks # Assert that our verifiable mock for New-ADuser cmdlet was called.
            $Createduser | Should  BeNullOrEmpty
        }

    } #end Context

} #end Describe


New-ADuserTemplate Function :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
 <#
.Synopsis
   Function which enables creating new users using a Template
.DESCRIPTION
   Function which will use a User as a template and then copy set of below attributes to the new user.

.EXAMPLE
    First get the AD user Stored in a variable with all the properties (it copies only a subset of properties on the Object supplied)
    PS> $TemplateUser = Get-ADUser -identity Test1 -Properties *
    PS> New-ADUserFromTemplate -SamAccountname newuser123 -GivenName NewUser -Instance $TemplateUser
.EXAMPLE
   If the AD User Object doesn't have all the Properties on it then the Function only selects the available ones.
    PS> $TemplateUser = Get-ADUser -identity Test1
    PS> New-ADUserFromTemplate -SamAccountname newuser123 -GivenName NewUser -Instance $TemplateUser
#>

function New-ADUserFromTemplate {

   param(
        # SPecify the unique SamAccountName for the User
        [Parameter(Mandatory)]              
        [ValidateNotNullOrEmpty()]
        [string]$SamAccountName,
  
        # Specify the First Name or Given name for the user
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String]$GivenName,

        [Parameter(Mandatory)]
        #[PSTyepName('Microsoft.ActiveDirectroy.Management.ADUser')]
        [Object]$Instance,

        [Switch]$Passthru
        )

        TRY {
            if ( -not (Get-Module -Name ActiveDirectory) ) {
                # try to import the Module
                Import-Module -name ActiveDirectory -ErrorAction stop
                $null = Get-PSDrive -Name AD -ErrorAction stop  # Query if the AD PSdrive is loaded
            }
        }
        CATCH [System.IO.FileNotFoundException]{
            Write-Warning -Message $_.exception
            throw "AD module not found"
        }
        CATCH {
            throw $_.exception
        }        

        # Let's start by following the link : https://technet.microsoft.com/en-us/library/dd378959(v=ws.10).aspx
        $Instance.UserPrincipalName = $Null

        if ($Passthru.IsPresent) {  
            New-ADuser -Name $SamAccountName -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False -Passthru
        }
        else {
          $null =  New-ADuser -Name $SamAccountname -SamAccountName $SamAccountName -GivenName $GivenName -Instance $Instance -Enabled $False
        } 

}

Resources:

Preivous post
http://www.dexterposh.com/2015/09/powershell-ad-pester-create-new-user.html

https://msdn.microsoft.com/en-us/library/ms679765(v=vs.85).aspx

http://www.hurryupandwait.io/blog/why-tdd-for-powershell-or-why-pester-or-why-unit-test-scripting-language

https://www.simple-talk.com/sysadmin/powershell/practical-powershell-unit-testing-getting-started/#eleventh

Read this post by Jakub where he explained about CQS principle, which I intend to follow in my future practices.
http://powershell.org/wp/2015/10/18/command-and-query-separation-in-pester-tests/

Popular posts from this blog

Test connectivity via a specific network interface

Recently while working on a Private cloud implementation, I came across a scenario where I needed to test connectivity of a node to the AD/DNS via multiple network adapters.  Many of us would know that having multiple network routes is usually done to take care of redundancy. So that if a network adapter goes down, one can use the other network interface to reach out to the node. In order to make it easy for everyone to follow along, below is an analogy for the above scenario: My laptop has multiple network adapters (say Wi-Fi and Ethernet) connected to the same network. Now how do I test connectivity to a Server on the network only over say Wi-Fi network adapter?

PowerShell + SCCM : Run CM cmdlets remotely

Today I saw a tweet about using implicit remoting to load the Configuration Manager on my machine by Justin Mathews . It caught my eye as I have never really tried it, but theoretically it can be done. Note - The second tweet says "Cannot find a provider with the name CMSite", resolution to which is in the Troubleshooting section at the end.

PowerShell : Trust network share to load modules & ps1

Problem Do you have a central network share, where you store all the scripts or PowerShell modules ? What happens if you try to run the script from a network share ? or if you have scripts (local) which invoke scripts or import PowerShell modules stored on this network share ? Well you would see a security warning like below (Note - I have set execution policy as 'Unrestricted' not 'bypass' here): Run a .ps1 from the network share Well this is a similar warning, which you get when you download scripts from Internet. As the message says run Unblock-File cmdlet to unblock the script and then run it, let's try it.