In software development world, test
driven development (TDD) is a well-recognized practice that the teams use to
improve software quality and design. Apart from having a clean and maintainable
code base, the team also benefits from a suite of automated tests, that are
executed as part of the continuous integration process used for providing
faster feedback cycles. Similar benefits can be gained in infrastructure
projects when infrastructure is treated as code driven by tests.
Test driven infrastructure is a
practice employed by highly efficient DevOps teams working on infrastructure
automation using configuration management tools such as PowerShell DSC, Chef
etc. to develop their infrastructure in code and provide a complete support to
introduce and run tests. This can allow development and operations teams to
collaborate and confidently deliver working infrastructure code.
If you are working in a team that
uses PowerShell DSC for configuration management and involve in creating and
using DSC resources, you should also start writing both unit and integration
tests for your resources and configurations. In this post I’ll explain a
scenario where we create a custom DSC resource (to install modules from
PowerShell repository) and later use that in a configuration to install the
required modules in our infrastructure. As the focus point is more on testing,
we’ll not be looking into the details of DSC and how we can create custom
resource etc. You can read about PowerShell DSC for more details here (https://msdn.microsoft.com/en-us/powershell/dsc/overview
). Also I’ll be using Pester (https://github.com/pester/Pester/wiki/Pester)
for creating BDD style tests for our configurations.
“Pester
is a BDD based test runner for PowerShell.
Pester provides a framework for running Unit Tests to execute and
validate PowerShell commands. Pester follows a file naming convention for naming
tests to be discovered by pester at test time and a simple set of functions
that expose a Testing DSL for isolating, running, evaluating and reporting the
results of PowerShell commands. Pester tests can execute any command or script
that is accessible to a pester test file. This can include functions, Cmdlets,
Modules and scripts. Pester can be run in ad hoc style in a console or it can
be integrated into the Build scripts of a Continuous Integration system.”
Creating a Pester test for our custom DSC resource.
Let’s directly jump into some code
that we will be testing. Below is the DSC resource that I have created for
installing modules using the cmdlets available in the PowerShellGet module.
function Get-TargetResource
{
[CmdletBinding()]
[OutputType([System.Collections.Hashtable])]
param
(
[parameter(Mandatory = $true)]
[System.String]
$Module
)
$moduleDetails = Get-Module -Name $Module -ListAvailable -ErrorAction SilentlyContinue
$present = $moduleDetails -ne $null
$presentValue = 'Absent'
if($present){
$presentValue = 'Present'
}
$returnValue = @{
Module = $moduleDetails | Select -expand Name
Version = $module | Select Version -ErrorAction SilentlyContinue | Format-Table -HideTableHeaders -ErrorAction SilentlyContinue | Out-String -ErrorAction SilentlyContinue
Ensure = $presentValue
}
return $returnValue
}
function Set-TargetResource
{
[CmdletBinding()]
param
(
[parameter(Mandatory = $true)]
[System.String]
$Module,
[System.String]
$Version,
[ValidateSet("Present","Absent")]
[System.String]
$Ensure
)
$present = $ensure -eq 'Present'
if($present){
if([string]::IsNullOrWhiteSpace($Version)){
"Installing module $Module" | Write-Verbose
Install-Module -Name $Module -Force -Verbose
}
else{
"Installing module $Module with version $Version" | Write-Verbose
Install-Module -Name $Module -RequiredVersion $Version -Force -Verbose
}
}
else {
if([string]::IsNullOrWhiteSpace($Version)){
"Uninstalling module $Module" | Write-Verbose
Uninstall-Module -Name $Module -Force -AllVersions
}
else{
"Uninstalling module $module with version $Version" | Write-Verbose
Uninstall-Module -Name $Module -Force -RequiredVersion $Version
}
}
}
function Test-TargetResource
{
[CmdletBinding()]
[OutputType([System.Boolean])]
param
(
[parameter(Mandatory = $true)]
[System.String]
$Module,
[System.String]
$Version,
[ValidateSet("Present","Absent")]
[System.String]
$Ensure
)
$result = (Get-Module -ListAvailable -Name $Module -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) -ne $null
$present = $Ensure -eq 'Present'
if($result){
if($present){
"The module $Module already exists. No action needed" | Write-Verbose
return $true
}
else {
"The module $Module exist. This will be removed" | Write-Verbose
return $false
}
}
else{
if($present){
"The module $Module does not exist. This will be
installed" | Write-Verbose
return $false
}
else{
"The module $Module does not exist. No action needed" | Write-Verbose
return $true
}
}
}
Export-ModuleMember -Function *-TargetResource
We have 3 functions in the module to
test. My goal is to create some unit tests to ensure that the
Get-TargetResource, Set-TargetResource and Test-TargetResource methods work as
expected.
Test 1:
Our first test is to check whether
the Test-TargetResource method returns a true if the Get-Module cmdlet finds a
module that is passed as parameter in the system. Remember we don’t need to
actually install the module in the system to test this scenario. I’ll be using
the Mocking feature of Pester to mock the Get-Module command and use those
results to test my method under test.
For Pester to test the functions in
the module, we need to allow Pester to launch the module. We can do that by copying
the contents of the module to a test script file and dot sourcing the file in
the test code. The next important step is to mock the cmdlets that we don’t
want to be invoked from the test script. In this case the Export-ModuleMember
and the Get-Module commands.
$currentFolder = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
$Module = 'xPSPackage'
$DSCResource = 'xPSModule'
#Replace this with the folder location
of your module
$moduleFolder = "$currentFolder\..\..\Resources\$Module"
Describe "$DSCResource
Test-TargetResource"{
Copy-Item "$moduleFolder\DSCResources\$DSCResource\$DSCResource.psm1" TestDrive:\script.ps1 -Force
Mock Get-Module { return "$Module" }
Mock Export-ModuleMember {return $true}
. "TestDrive:\script.ps1"
Context "CChoco
module is installed and Ensure is passed as Present"{
It "Should return true"{
Test-TargetResource -Module "CChoco" -Ensure "Present" | Should Be $True
}
}
}
We can try to execute the test and see
the results. In the command prompt type Invoke-Pester
Test 2:
After the Test-TargetResource, the
next step is to test a more complex scenario like the Set-TargetResource. Here
we have multiple conditions to test like.
- .
Installing a module without
mentioning a version
- .
Installing a module with a
specific version
- .
Uninstall an existing module
We’ll use the same test file and add
another “Describe” block for testing the Set-TargetResource function. We’ll
also be mocking the Install-Module and Uninstall-Module commands from the
PowerShellGet module but we’ll be using verifiable mocks to ensure that these
commands when called were called with the right parameters and values.
Describe "$DSCResource
Set-TargetResource"{
Copy-Item "$moduleFolder\DSCResources\$DSCResource\$DSCResource.psm1" TestDrive:\script.ps1 -Force
Mock Export-ModuleMember {return $true}
. "TestDrive:\script.ps1"
Context "Ensure
is passed as Present"{
Mock Install-Module -Verifiable
It "Should call install module"{
Set-TargetResource -Module "CChoco" -Ensure 'Present'
Assert-VerifiableMocks
}
}
Context "Ensure
is passed as Present and Version as 2.0.0"{
Mock Install-Module -Verifiable -ParameterFilter {
$Version -eq '2.0.0'
}
It "Should call install module with version 2.0.0"{
Set-TargetResource -Module "CChoco" -Ensure 'Present' -Version '2.0.0'
Assert-VerifiableMocks
}
}
Context "Ensure
is passed as Absent"{
Mock Uninstall-Module -Verifiable
It "Should call Remove module"{
Set-TargetResource -Module "CChoco" -Ensure 'Absent'
Assert-VerifiableMocks
}
}
}
The code looks similar to the one
before, except for the Assert-VerifiableMocks call. You can execute the tests
again by using the Invoke-Pester command.
Creating Pester tests for testing the configuration.
Now it’s time to use the custom DSC
resource in a configuration and run it against a node. After executing the
configuration we would like to test whether the node is now in the desired
state or not. Before executing these tests, we need to create a configuration
that uses the xPSModule we created and apply the configuration on the target
node. I’ve create a sample configuration that will install 3 modules (cChoco,
Octopus-Cmdlets and VSTS) on the machine. Later we’ll apply this configuration
and create some tests to verify the state of the node after applying the configuration.
Configuration PSModuleConfiguration{
Import-DSCResource -Module xPSPackage
Node 'localhost'{
xPSModule Chocolatey{
Module = 'cChoco'
Ensure = 'Present'
}
xPSModule Octopus{
Module = 'Octopus-Cmdlets'
Ensure = 'Present'
}
xPSModule VSTS{
Module = 'VSTS'
Ensure = 'Present'
}
}
}
PSModuleConfiguration
Start-DSCConfiguration .\PSModuleConfiguration -Wait -Verbose
Now we can create some tests to
verify the configuration. I’ve created different folders for my unit and
integration tests. In the integration test folder add a new script file and
copy the contents below to the script file.
Describe "Installation of modules from
PowerShellGet"{
Context "PSModuleConfiguration
is applied on the system"{
It "Should have installed CChoco module"{
Get-Module -Name "cChoco" -ListAvailable | Should Not BeNullOrEmpty
}
It "Should have installed Octopus cmdlets module"{
Get-Module -Name "Octopus-Cmdlets" -ListAvailable | Should Not BeNullOrEmpty
}
It "Should have installed VSTS module"{
Get-Module -Name "VSTS" -ListAvailable | Should Not BeNullOrEmpty
}
}
}
We are expecting the configuration to
be applied on the node, before executing these tests. In the test, we are later
asserting whether the modules that were installed as part of the configuration
are available in the node. Use the Invoke-Pester command to execute the tests
and verify the results.