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 :
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 :
- Context "User Creation"
It should return Object when -Passthru specified (New addition)
It should take OU Path from template User.
It shouldonlycopy 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.
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 :
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).
The reason to use the dummy module are :
Now let's take a look at the test.
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.
Now if I run the test then it should pass (Green phase) this particular unit test.
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.
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.
If you have anything to add on to my current approach then the feedback is much appreciated.
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/
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 :
- Pester tests don't depend on AD PowerShell module.
- 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 :
Now all the tests are passing, below is a screenshot showing that.
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 shouldonly copy allowed set of attributes from the User (by default).
It should allow copying a subset of allowed set of attributes.
It should take OU Path from template User.
It should
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
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 :
New-ADuserTemplate Function :
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 posthttp://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/