Wednesday, July 27, 2016

Test driven infrastructure using Pester and PowerShell DSC

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.



















No comments: