- Each DSC module should contain the following Test folder structure:
Tests
|---Unit
|---Integration
- The Tests\Unit folder must contain a Test Script for each DSC Resource in the DSC Module with the filename
MSFT_<ResourceName>.tests.ps1
. - The Tests\Integration folder should, whenever possible, contain a Test Script for each DSC Resource in the DSC Module with the filename
MSFT_<ResourceName>.integration.tests.ps1
. - Each Test Script should contain Pester based tests.
- Integration tests should be created when possible, but for some DSC resources this may not be possible. For example, when integration tests would cause the testing computer configuration to be damaged.
- Unit and Integration tests should be created using the Template files that are located in the DscResource.Template repository.
- The Unit and Integration test templates require that
Git.exe
be installed and can be found in the%PATH%
on the testing computer. - The Unit and Integration test templates will automatically download or update the DSCResource.Tests repository using Git.exe to the DSC Module folder.
- Ensure that the .gitignore file for the resource module contains DSCResource.Tests so that this folder is not accidentally included in a commit to your resource repository.
- Ensure that when creating or updating unit/integration tests from the Test.Templates the Version number of the test template file used remains in the test file.
- Copy the
unit_test_template.ps1
to the\Tests\Unit\
folder and rename toMSFT_<ResourceName>.tests.ps1
- Open
MSFT_<ResourceName>.tests.ps1
and customize TODO sections.- Note: The existing Describe blocks do not have to be used. The blocks included are a common test pattern, but may be completely replaced with a more suitable pattern.
Important: Please ensure that the test files created retain the version number of the template used to create them.
- Copy the
integration_test_template.ps1
to\Tests\Integration\
folder and renameMSFT_<ResourceName>.Integration.tests.ps1
- Open
MSFT_<ResourceName>.Integration.tests.ps1
and customize TODO sections. - Copy the
integration_test_template.config.ps1
to\Tests\Integration\
folder and renameMSFT_<ResourceName>.config.ps1
- Open
MSFT_<ResourceName>.config.ps1
and customize TODO sections.
Important: Please ensure that the test files created retain the version number of the template used to create them.
It is possible that the Unit and Integration test templates may change in future, either to support new functionality or fix an issue. When a change is made to a template, it will be documented in the change log of the TEMPLATE_README.md in the repository DscResource.Template. The version number in any current tests can be compared with the Change List to determine what has changed since the tests were updated. If any unit or integration tests based on these templates require any enhancements or fixes then they will need to be updated manually.
Important: Please ensure that when updating a test with the new Test Template content that the Template version number is also updated to match the new template.
All tests are automatically run via AppVeyor when the repository is updated. However, it is recommended that all tests be run locally before being committed.
Requirements for running all tests locally:
- Git is installed and the
Git.exe
can be found in the%PATH%
variable. - The latest Pester Module is installed, either manually or via PowerShellGet.
- An internet connection is available so that the DSCResource.Tests repository can be downloaded or updated using Git.
If a resource contains private (non-exported) functions that need to be tested, then to allow these functions to be tested by Pester, they must be contained in an InModuleScope
Pester block:
InModuleScope $Global:DSCResourceName {
Describe "$($Global:DSCResourceName)\Get-FirewallRuleProperty" {
# Test Get-FirewallRuleProperty private/non-exported function
}
}
Note: The core DSC Resource functions Get-TargetResource
, Set-TargetResource
and Test-TargetResource
must always be public functions, so do not need to be contained in an InModuleScope
block.
It is common for modules to declare variables that are needed inside the module, but may also be required for unit testing.
One common example of this is the LocalizedData
variable that contains localized messages used by the resource.
Variables declared at the module scope (private variables) can not be accessed by unit tests that are not inside an InModuleScope
Pester block.
Note: See Localization section for more information of using localized messages in tests.
There are two solutions to this:
- Use the
InModuleScope
to copy the private variable into a variable scoped to the Unit test:
$LocalizedData = InModuleScope $Global:DSCResourceName {
$LocalizedData
}
- Add any variables that are required to be accessed outside the module by unit tests to the
Export-ModuleMember
cmdlet in the DSC Resource:
Export-ModuleMember -Function *-TargetResource -Variables LocalizedData
One useful pattern used when creating unit tests is to create variables that contain objects that will be returned when mocked cmdlets are called. These variables are often used many times over a single unit test file and so assigning each to a variable is essential to reduce the size of the unit test as well as improve readability and maintainability. For example:
$MockNetAdapter = @{
Name = 'Ethernet'
PhysicalMediaType = '802.3'
Status = 'Up'
}
# Create a mock
Mock `
-CommandName Get-NetAdapter `
-MockWith { return $MockNetAdapter }
However, when InModuleScope
is being used the variables that are defined won't be accessible from within the mocked cmdlets.
The solution to this is create a script block variable that contains the mocked object and then assign that to the Mock.
$GetNetAdapter_PhysicalNetAdapterMock = {
return @{
Name = 'Ethernet'
PhysicalMediaType = '802.3'
Status = 'Up'
}
}
# Create a mock
Mock `
-CommandName Get-NetAdapter `
-ModuleName $script:ModuleName `
-MockWith $GetNetAdapter_PhysicalNetAdapterMock
Pattern 1: The goal of this pattern should be to describe the potential states a system could be in for each function.
Describe 'Get-TargetResource' {
# TODO: Complete Get-TargetResource Tests...
}
Describe 'Set-TargetResource' {
# TODO: Complete Set-TargetResource Tests...
}
Describe 'Test-TargetResource' {
# TODO: Complete Test-TargetResource Tests...
}
Pattern 2: The goal of this pattern should be to describe the potential states a system could be in so that the get/set/test cmdlets can be tested in those states. Any mocks that relate to that specific state can be included in the relevant Describe block. For a more detailed description of this approach please review #143
Add as many of these example 'states' as required to simulate the scenarios that the DSC resource is designed to work with, below a simple 'is in desired state' and 'is not in desired state' are used, but there may be more complex combinations of factors, depending on how complex your resource is.
Describe 'The system is not in the desired state' {
#TODO: Mock cmdlets here that represent the system not being in the desired state
#TODO: Create a set of parameters to test your get/set/test methods in this state
$testParameters = @{
Property1 = 'value'
Property2 = 'value'
}
#TODO: Update the assertions below to align with the expected results of this state
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
It 'Should return false' {
Test-TargetResource @testParameters | Should -Be $false
}
It 'Should call Demo-CmdletName' {
Set-TargetResource @testParameters
#TODO: Assert that the appropriate cmdlets were called
Assert-MockCalled Demo-CmdletName
}
}
Describe 'The system is in the desired state' {
#TODO: Mock cmdlets here that represent the system being in the desired state
#TODO: Create a set of parameters to test your get/set/test methods in this state
$testParameters = @{
Property1 = 'value'
Property2 = 'value'
}
#TODO: Update the assertions below to align with the expected results of this state
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
It 'Should return true' {
Test-TargetResource @testParameters | Should -Be $true
}
}
To see examples of the Unit/Integration tests in practice, see the NetworkingDsc MSFT_DhcpClient resource:
When resources use localization (which all normally should) there is the possibility to use the localized messages in the tests. This is normally used to verify the correct error messages are thrown. But could be used for verbose messages, warning messages, and of course any other types.
When using the
Get-localizedData
helper function to load the localized strings into $script:localizedData
in
the resource module script file, the strings can easily be used like this in
tests.
Note: For this to work the It-block must be inside a
InModuleScope
-block.
It 'Should throw the correct error message' {
{
Set-TargetResource @setTargetResourceParameters
} | Should -Throw $script:localizedData.DatabaseMailDisabled
}
There are two helper functions to simplify testing localized error messages. The helper functions can be used to build a correct localized error record to be used in the tests. They are used together with the Helper functions for localization.
These test helper functions can currently only be found in other resource modules, for example in the resource module NetworkingDsc, in the CommonTestHelper module. To use it, copy the PowerShell module CommonTestHelper.psm1 to the new resource module.
This helper function returns an invalid argument exception error object. Below is an example of how it could look like in the tests to test that the correct localized error record is returned.
$errorRecord = Get-InvalidArgumentRecord `
-Message ($LocalizedData.InterfaceNotAvailableError -f $interfaceAlias) `
-ArgumentName 'Interface'
It 'Should throw an InterfaceNotAvailable error' {
{ Assert-ResourceProperty @testRoute } | Should -Throw $errorRecord
}
This helper function returns an invalid operation exception error object. Below is an example of how it could look like in the tests to test that the correct localized error record is returned.
$errorRecord = Get-InvalidOperationRecord `
-Message ($LocalizedData.NetAdapterNotFoundError)
It 'Should throw the correct exception' {
{
$script:result = Find-NetworkAdapter -Name 'NoMatch'
} | Should -Throw $errorRecord
}