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).
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.
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 :
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:
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).
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 :
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 :
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 :
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 :
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 :
Below is the result of my pester tests in Green phase :
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.
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
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.
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.
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.
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 |
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 givenname) as 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.
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:
- Context "Help and Parameter Checks"
It should have inbuilt help along with Description and examples.
It should have SamAccountName , GivenName & Instance as mandatory parameters. - 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) - 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.
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