Saturday, September 12, 2015

PowerShell + AD + Pester : Create new user using template - Day 1

I did a blog post, way back to create new users in AD using already existing user as a template, but many people commented about using the template didn't copy the Home Directory, logon script path, group membership etc. So finally I tried my hands on writing a Function which does a better job at this.

The idea is to write a New-ADUserFromTemplate function, to which you specify all the properties you want copied while creating a User from an existing User (template User).


Let's make it fun and write the code using the Behavior Driven development approach using Pester. This will probably a 2 part series :

  • Day 1 - Getting the Ground Work ready, Pester tests for Parameter, Help & Module dependency.
  • Day 2 - Write Pester tests and code for the actual function. Refactoring the Code.

So we plan to do BDD or TDD here which means we write tests first and then follow the below cycle :






Disclaimer - Not an expert on BDD/TDD , but constructive feedback is always welcomed.



Let's start by creating a fixture named New-ADUserTemplate using the New-Fixture Function:
PS>New-Fixture -Path .\New-ADUserFromTemplate -Name New-ADUserFromTemplate 
This creates a folder named NewADUserFromTemplate with two files inside it. 
PS>ls

    Directory: C:\Users\Deepak_Dhami\Documents\WindowsPowerShell\Scripts\New-ADUserFromTemplate


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         8/16/2015   7:43 AM       2054 New-ADUserFromTemplate.ps1
-a---         8/16/2015   6:39 AM       2162 New-ADUserFromTemplate.Tests.ps1


First one is an empty PS1 file with an empty Function definition for New-ADUserFromTemplate and other one matching *.Tests.ps1 is the File where our tests will live. If you haven't picked up on Pester yet then check out the resources section.

So we start by writing tests which define behavior of our code, at first we have any  empty function. So obviously our tests will fail (have patience). Now the next thing on our mind should not be to write an advanced function in PowerShell, instead write simplest code which passes all test. The idea is to get it right first before putting in all fancy stuff there.

Once the test start passing which means my code has achieved the behavior, I wanted.Now it is time to refactor the code (keep running tests when you modify code to make sure the code's behavior hasn't changed).



You won't realize the tremendous benefit this will have to your Scripting effforts until your Function or Module starts growing exponentially and when it does you would be thankful to yourself that you modeled the code properly from start. Below pic sums what I meant to say here :

Credits - memegenerator.net
So let's do it.

First determine the behavior of the Function which we are going to write. The thing which comes to my mind at first is to make sure that I put help and correct parameters in my function.

For the code I am writing I want to mimic my parameters to the ones which the AD User and Computers Snap-in GUI provides while copying a User, using a template. (select User > Right click "Copy").




Let's define the behavior of the Function:
  • It should have inbuilt help along with Description and examples.
  • It should have SamAccountName and GivenName (FirstName is givennameas Mandatory parameters. Also it should have mandatory Instance parameter which takes a AD User Object as input to use as a template.
  • My Function will depend on ActiveDirectory PS Module.
  • It should take the OU path from the Template User.
  • Based on some Constraints in AD Schema, we can only copy few attributes from a template User. Make sure allowed attributes if present on the template User are copied to the new User.
  • It should allow us to select a subset of allowed attributes to copy.
Wow that is easy, now let's rewrite our function's behavior in Pester. I will write the Pester tests and side by side refactor my code so that tests pass. 

To begin with Pester tests have this File preamble , where we dot source our Function/Module which we will test along with any helper functions. Below is how a Pester tests file looks :



Note that I named my Describe block "New-ADUserFromTemplate", as that is exactly what I am doing describing behavior of my Function to Pester.

In file preamble, I load the New-ADUserTemplate.ps1 along with HelperFunctions.ps1 (contains 2 functions named : Test-MandatoryParam & Compare-ADUser).
 

I tend to organize my tests in the Context of the testing I am doing. So you would see in above screenshot that there are 3 Context blocks which are organized in my mind in below way:
  1. Context "Help and Parameter Checks"
      
    It should have inbuilt help along with Description and examples.
      It should have SamAccountName GivenName & Instance as mandatory parameters.
  2. Context "ActiveDirectory Module Checks"
      It Should fail if the AD Module not present.
      It Should fail if the AD PSDrive is not loaded. (Extreme Case)
  3. 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.  
See piece of cake, If you have thought over, how the code will behave before hand and written tests describing it then half of the job is done.

Let's start with the first Context block 
"Help and Parameter Checks".
Inside the Context block, we have It block which is essentially a unit test. It is inside this IT block that we put our assertions (in layman terms these are comparison between the expected and actual output etc.).
Below is  how my first Context block looks like :
   
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
    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

First IT block tests that my Function help always has Examples, Details and Description. Credits to Andy Schnieder for sharing this.

Second IT block is a bit tricky here. I defined that the parameters SamAccountName and GivenName be mandatory parameters for my Function. Now the first assertion which naturally comes to mind is :
{New-ADuserFromTemplate| should throw 

But this won't work, we will see later why. So as a temporary workaround of not specifying anything to Mandatory parameters, I am passing them $Null.
{New-ADuserFromTemplate -samAccountName $null } | should throw

There are some otherways to look at the Function metadata (MVP Dave Wyatt pointed out that to me) but that will make this post deviate from the original objective. So my tests which define the help & parameters behavior on my function are ready.

Let's go through the Red phase first :

PS>invoke-pester -TestName 'New-ADUserFromTemplate' 
Executing all tests in 'C:\Users\DexterPOSH\Documents\WindowsPowerShell\Pester_Tests\new-aduserfromtemplate' matching test name 'New-ADUserFromTemplate'
Describing New-ADUserFromTemplate
   Context Help and Parameter checks
    [-] should have inbuilt help along with Description and examples 204ms
      Expected: value to not be empty
      at line: 16 in C:\Users\Deepak_Dhami\Documents\WindowsPowerShell\Pester_Tests\new-aduserfromtemplate\New-ADUserFromTemplate.Tests.ps1
    [-] Should have SamAccountName & GivenName as Mandatory params 37ms
      Expected: the expression to throw an exception
      at line: 22 in C:\Users\Deepak_Dhami\Documents\WindowsPowerShell\Pester_Tests\new-aduserfromtemplate\New-ADUserFromTemplate.Tests.ps1

All right, now let's go and work on making these tests pass by modifying our New-ADUserFromTemplate function , below is what I added to the Function definition :

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
function New-ADUserFromTemplate
{
<#
.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
#>

[CmdletBinding()]
   param(
        [Parameter(Mandatory=$True)]    
        [string]$SamAccountName,

        [Parameter(Mandatory)]    
        [string]$GivenName,

        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADUser]$Instance
   )
}

Once this is done, I invoke the Pester again and see the test in the Context passing, But :




Let's remove the blocking test and run the Pester tests again, we see Green :



Now it is time to refactor. Make sure that after any changes you make, run Pester to validate that nothing has changed.

Moving on to the next Context of testing. It looks like below :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
    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 -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
        }     
    }

Note that in the Context scope, a Custom Object $TemplateUser in initialized, which will be passed later on to the Function (mandatory).

Now , the problem is my machine has the AD Module, so how does I simulate a situation where the machine doesn't have the AD module. This is where mocking comes into picture.


In a machine where the AD module is present, if I run below: 

Import-Module -name ActiveDirectory -ErrorAction stop

It would succeed, but in a machine which doesn't have the module named ActiveDirectory an exception will be thrown. This would be our mock.
Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} `
     -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable 

Above, We mock -> Import-Module -name ActiveDirectory . Notice -ParameterFilter {$name -eq 'ActiveDirectory' mocks only the Import-Module cmdlet when it is passed an argument 'ActiveDirectory' to the -Name parameter. 

Also the  -Verifiable switch at the end of the mock makes it verifiable, how ?
Simple use 
Assert-VerifiableMocks at the end in the IT block. It will verify that the mocks with -Verifiable switch were called in the Function run.

After writing tests, run them (Red Phase), then write the bare minimum code to make it pass (Green) and keep refactoring. Skipping Red and jumping to Green for the above context (as you already have an idea).

My bare minimum Function definition passing both the Context tests is below :


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
function New-ADUserFromTemplate
{
<#
.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
#>

[CmdletBinding()]
   param(
        [Parameter(Mandatory=$True)]     
        [string]$SamAccountName,

        [Parameter(Mandatory)]     
        [string]$GivenName,

        [Parameter(Mandatory)]
        [Object]$Instance
   )
    TRY {
        # 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
    }
}

Below is the result of my pester tests in Green phase :


PS>invoke-pester

Describing New-ADUserFromTemplate
   Context Help and Parameter checks
    [+] should have inbuilt help along with Description and examples 233ms
    [+] Should have SamAccountName, GivenName  & Instance Mandatory params 57ms
   Context ActiveDirectory Module Available
WARNING: Unable to find the specified file.
    [+] Should Fail if the AD Module not present 106ms


Oh My God !  Why to go to this length trouble for a small function ?

You are right ! It is a whole lot of trouble but when this function starts to grow or it becomes part of a bigger module or Script running in Production. Running tests and seeing them pass would give you "Confidence" or "Trust" in your code.

Also you don't have to test each scenario manually ;)

In next post, we will dive into writing tests (first) and code which copies the attributes for a User from a template user. I am presenting on the very same topic at PowerShell Conference Asia @ Singapore next week and I hope to make a strong case for Pester there.

Resources:


Practical PowerShell Unit Testing : Getting Started (Fantastic article)
https://www.simple-talk.com/sysadmin/powershell/practical-powershell-unit-testing-getting-started/


PowerShellMag articles on Pester:
http://www.powershellmagazine.com/tag/pester/

Copy User's Properties

https://technet.microsoft.com/en-us/library/dd378959(v=ws.10).aspx

AD Constraints

https://msdn.microsoft.com/en-us/library/cc223462.aspx




9 comments:

  1. Great article! Nice illustration of ways to use Pester tests!

    ReplyDelete
  2. Instead of manually specifying properties you could use the AD schema which is what the gui uses when copying. See my noob script here:

    https://community.spiceworks.com/scripts/show_download/3323

    ReplyDelete
    Replies
    1. Thanks :) for the tip. Will try using it.

      Delete
    2. You're welcome. This is looking awesome! Excited for the next post.

      Delete
    3. This comment has been removed by the author.

      Delete
    4. Cool, I have it almost drafted in my mind ;)

      The AD Schema tricks looks quite neat, I can probably show that in the Refactor phase and give you credits.

      Thanks for sharing it.

      Delete
  3. Thanks for writing this up, it's one of the best intros to Pester that I've seen so far. I did have a question though, between part 1 and part 2 the mock for module dependency changed from this:

    Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable
    Assert-VerifiableMocks

    To this:

    Mock Get-Module -MockWith {$Null}
    Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable
    Assert-VerifiableMocks

    Can you speak a little to what that first "Mock Get-Module -MockWith {$Null}" is looking for? Is it simply checking to see if there are no modules loaded? I've also been seeing this error randomly when checking module dependencies, any thoughts?

    Expected Import-Module to be called with $name -eq 'ActiveDirectory'
    at line: 337 in C:\Program Files\WindowsPowerShell\Modules\pester\3.3.5\Functions\Mock.ps1

    ReplyDelete
    Replies
    1. Hi ITFields,

      First of all thanks for the comment. I was expecting this question earlier, this means you paid attention between the 2 posts.

      Let me break it down, in Part 1 the module loading logic was something like below:
      TRY {
      # 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"
      }

      This worked fine until I started writing the part 2 where I realized that my unit tests should run independently of the AD PS module, so I started cheating by loading a dummy AD module in the Context block as mentioned in part 2.

      But the module loading logic used in Part 1 won't cut this anymore, as I was doing Import-Module directly so it needed me to drop the dummy AD module created to one of the PSModulepath, which I didn't want to do. So instead I improvised the module loading logic to below:

      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
      }
      }
      So now the Get-Module (which looks for module loaded in the memory) checks if the AD module is already loaded. This will be able to see my dummy AD module which Pester injected and the Import-Module will never run.

      Now coming to the Mocking part.
      Mock Get-Module -MockWith {$Null}
      Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable
      Assert-VerifiableMocks

      The first mock for the Get-Module will make sure that the logic enters the Import-Module and then the second mock for the Import-Module will mock it and throw the FileNotFound Exception.
      This will validate the assertion I have :
      {New-ADUserFromTemplate -SamAccountName test123 -GivenName 'test 123' -Instance $TemplateUser } | should throw

      Also the mock are verifiable so the Assert-VerifiableMocks will validate that those mocks were called inside my function.

      Hope that makes sense.
      Let me know, I would be happy to help you figure this out more.

      Dex

      Delete