-
-
Notifications
You must be signed in to change notification settings - Fork 144
Writing a New Check
Thank you for adding to this awesome project, it would not be possible without you all adding your knowledge and expertise.
First clone the repo, created a new local branch (You can see instructions here if you are new to git)
We want to follow Jakubs methodology described here and Michals post This ensures that our tests are passing and failing as expeceted and that any alterations to the check do not break exisitng functionality
The first thing to do is not to write the check or the test but to think about exactly what is required to be tested, what will be configurable and what the results will look like for a failure and for a succesful check. We would also need to consider default values for our configuration items
Lets use the instance level MaxDop as an example
We want to check that the instance level maxdop setting is set to a configurable value so we will need a configuration item for that. However we may also want to use the recommended values from Test-DbaMaxDop so that is another configuration item. As we may have instances in our estate that may require different settings (Sharepoint) then we would want to be able to specify them to be excluded
So our logic flow would be
- if this instance is not in the exclusion list
- if we want to use the recommended values
- check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the RecommendedMaxDop property
- or if we want to use specific values
- check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the configuration item value
- if we want to use the recommended values
In this example we need 3 configuration items
Do we want to use the recommended values - true or false
What value do we expect - int
Whcih instances shall we exclude - array of strings
Open the .\internal\configurations.configurations.ps1 file and add the 3 configurations using one of the other entries as an example. So I added
# InstanceMaxDop
Set-PSFConfig -Module dbachecks -Name policy.instancemaxdop.userecommended -Value $false -Initialize -Description "Use the recommendation from Test-DbaMaxDop to test the Max DOP settings - If set to false the value in policy.instancemaxdop.maxdop is used"
Set-PSFConfig -Module dbachecks -Name policy.instancemaxdop.maxdop -Value 0 -Initialize -Description "The value for the Instance Level MaxDop Settings we expect"
Set-PSFConfig -Module dbachecks -Name policy.instancemaxdop.excludeinstance -Initialize -Description "Any Instances to exclude from checking Instance Level MaxDop - Useful if your estate contains SQL instances supporting Sharepoint for example"
We need to ensure that we have written a good description explaining what the configuration item is for and that the naming of the configuration item makes sense.
So we will write a function that will perform our check and then we will write a unit test that will check that the function is performing as expected
Here is an example for the MaxDop Checks
I will start with the function (I know that this is not how to do TDD but it works in this scenario)
In the internal\assertions folder I created a new file called Assert-InstanceMaxDop.ps1 and start it like this with parameters that match the configuration items and an Instance parameter
function Assert-InstanceMaxDop {
Param(
[string]$Instance,
[switch]$UseRecommended,
[int]$MaxDopValue
)
}
then I add my logic flow as comments so that I know how I want to write my code
function Assert-InstanceMaxDop {
Param(
[string]$Instance,
[switch]$UseRecommended,
[int]$MaxDopValue
)
#if UseRecommended - check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the the RecommendedMaxDop property
#if not UseRecommended - check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
}
Then I can start coding the function
function Assert-InstanceMaxDop {
Param(
[string]$Instance,
[switch]$UseRecommended,
[int]$MaxDopValue
)
$MaxDop = (Test-DbaMaxDop -SqlInstance $Instance)[0]
if ($UseRecommended) {
#if UseRecommended - check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the the RecommendedMaxDop property
}
else {
#if not UseRecommended - check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
}
}
and then
function Assert-InstanceMaxDop {
Param(
[string]$Instance,
[switch]$UseRecommended,
[int]$MaxDopValue
)
$MaxDop = (Test-DbaMaxDop -SqlInstance $Instance)[0]
if ($UseRecommended) {
#if UseRecommended - check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the the RecommendedMaxDop property
$MaxDop.CurrentInstanceMaxDop | Should -Be $MaxDop.RecommendedMaxDop -Because "We expect the MaxDop Setting $($MaxDop.CurrentInstanceMaxDop) to be the recommended value $($MaxDop.RecommendedMaxDop)"
}
else {
#if not UseRecommended - check that the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
$MaxDop.CurrentInstanceMaxDop | Should -Be $MaxDopValue -Because "We expect the MaxDop Setting $($MaxDop.CurrentInstanceMaxDop) to be $MaxDopValue"
}
}
I can test that on a SQL instance if I wish using
## Check the Use recommended
Assert-InstanceMaxDop -Instance SQL0 -UseRecommended
## Check the Maxdop value for incorrect value
Assert-InstanceMaxDop -Instance SQL0 -MaxDopValue 4
## Check the MaxDop Value for a correct value - Note this will not give an output
Assert-InstanceMaxDop -Instance SQL0 -MaxDopValue 0
open .\tests\checks\Instancechecks.Tests.ps1 and add a Context block and understand what we will be testing
Describe "Checking Instance.Tests.ps1 checks" -Tag UnitTest {
Context "Checking Backup Compression" {..
}
Context "Checking Instance MaxDop" {
# if recommended it should pass if CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the RecommendedMaxDop property
# if recommended it should fail if CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the RecommendedMaxDop property
# if not UseRecommended - it should pass if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
# if not UseRecommended - it should fail if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the MaxDopValue parameter
}
}
Now we need to build our test cases for running our unit tests for the check. this is exmple is relatively straight forward bu tyou can see other examples in the InstanceCheck.Tests.ps1 file
Context "Checking Instance MaxDop" {
# if Userecommended it should pass if CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the RecommendedMaxDop property
# if Userecommended it should fail if CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the RecommendedMaxDop property
$TestCases = @{"MaxDopValue" = 5}
# if not UseRecommended - it should pass if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
# if not UseRecommended - it should fail if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the MaxDopValue parameter
}
Now we can start write our unit test using the test cases. You can read more about test cases here First we create an It block with a set of parameters from the test cases and reference them in the title inside <>
Context "Checking Instance MaxDop" {
# if Userecommended it should pass if CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the RecommendedMaxDop property
It "Passes Check Correctly with the use recommended parameter set to true" {
}
# if Userecommended it should fail if CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the RecommendedMaxDop property
It "Fails Check Correctly with the use recommended parameter set to true" {
}
$TestCases = @{"MaxDopValue" = 5}
# if not UseRecommended - it should pass if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
It "Passes Check Correctly with a specified value" -TestCases $TestCases {
Param($MaxDopValue)
}
$TestCases = @{"MaxDopValue" = 5},@{"MaxDopValue" = 0}
# if not UseRecommended - it should fail if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the MaxDopValue parameter
It "Fails Check Correctly with with a specified value" -TestCases $TestCases {
Param($MaxDopValue)
}
}
Now we need to mock the results of Test-DbaMaxDop to provide the results that are eeded for the unit tests.
Context "Checking Instance MaxDop" {
# if Userecommended it should pass if CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the RecommendedMaxDop property
It "Passes Check Correctly with the use recommended parameter set to true" {
# Mock to pass
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 0; "RecommendedMaxDop" = 0}}
}
# if Userecommended it should fail if CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the RecommendedMaxDop property
It "Fails Check Correctly with the use recommended parameter set to true" {
# Mock to fail
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 0; "RecommendedMaxDop" = 5}}
}
$TestCases = @{"MaxDopValue" = 5}
# if not UseRecommended - it should pass if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
It "Passes Check Correctly with a specified value" -TestCases $TestCases {
Param($MaxDopValue)
# Mock to pass
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 5; "RecommendedMaxDop" = $MaxDopValue}}
}
# if not UseRecommended - it should fail if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the MaxDopValue parameter
It "Fails Check Correctly with with a specified value" -TestCases $TestCases {
Param($MaxDopValue)
# Mock to fail
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 0; "RecommendedMaxDop" = 73}}
}
}
Then we can write our unit test, for passing the test we do not need a | Should for failing a test we need to put the call to the Asserrt-* function inside {} and any variables that are in the Expected message may need to be escaped with `$
Context "Checking Instance MaxDop" {
# if Userecommended it should pass if CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the RecommendedMaxDop property
It "Passes Check Correctly with the use recommended parameter set to true" {
# Mock to pass
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 0; "RecommendedMaxDop" = 0}}
Assert-InstanceMaxDop -Instance 'Dummy' -UseRecommended
}
# if Userecommended it should fail if CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the RecommendedMaxDop property
It "Fails Check Correctly with the use recommended parameter set to true" {
# Mock to fail
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 0; "RecommendedMaxDop" = 5}}
{Assert-InstanceMaxDop -Instance 'Dummy' -UseRecommended} | Should -Throw -ExpectedMessage "Expected 5, because We expect the MaxDop Setting 0 to be the recommended value 5"
}
$TestCases = @{"MaxDopValue" = 5}
# if not UseRecommended - it should pass if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop matches the MaxDopValue parameter
It "Passes Check Correctly with a specified value <MaxDopValue>" -TestCases $TestCases {
Param($MaxDopValue)
# Mock to pass
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 5; "RecommendedMaxDop" = $MaxDopValue}}
Assert-InstanceMaxDop -Instance 'Dummy' -MaxDopValue $MaxDopValue
}
$TestCases = @{"MaxDopValue" = 5}, @{"MaxDopValue" = 0}
# if not UseRecommended - it should fail if the CurrentInstanceMaxDop property returned from Test-DbaMaxDop does not match the MaxDopValue parameter
It "Fails Check Correctly with with a specified value <MaxDopValue>" -TestCases $TestCases {
Param($MaxDopValue)
# Mock to fail
Mock Test-DbaMaxDop {@{"CurrentInstanceMaxDop" = 4; "RecommendedMaxDop" = 73}}
{Assert-InstanceMaxDop -Instance 'Dummy' -MaxDopValue $MaxDopValue} | Should -Throw -ExpectedMessage "Expected $MaxDopValue, because We expect the MaxDop Setting 4 to be $MaxDopValue"
}
# Validate we have called the mock the correct number of times
It "Should call the mocks" {
$assertMockParams = @{
'CommandName' = 'Test-DbaMaxDop'
'Times' = 5
'Exactly' = $true
}
Assert-MockCalled @assertMockParams
}
}
You will see that I have also added a test to assert that the Mocked function has been called the correct number of times. I would run the test with
Invoke-Pester .\tests\checks\InstanceChecks.Tests.ps1
If you have trouble getting the mocks to work as you expect you can always use Write-Verbose to see what values are being passed, So you can add this to the Assert function
$MaxDop = @(Test-DbaMaxDop -SqlInstance $Instance)[0]
Write-Verbose -Message "Current = $($MaxDop.CurrentInstanceMaxDop)"
Write-Verbose -Message "Recommended = $($MaxDop.RecommendedMaxDop)"
Write-Verbose -Message "MaxDopValue = $MaxDopValue"
and then when you run the unit tests you can set $VerbosePreferences = 'Continue' and you will get the verbose output and can see what is happening
NOTE - This will not protect us from any alterations to the underlying dbatools command so for example if a parameter name or property name is changed in dbatools that would not be reflected here. We should be trying to add the relevenat unit test to the dbatools project so that it will fail if it wil break dbachecks and then we can have a discussion about the best way forward (probably altering dbachecks to use the new value) but that is beyond the scope of this post.
Now that we have our function for the test and the unit test to make sure it passes and fails as expected we now need to add it to the correct checks file. This example is using Instance Max Dop so we will addd it to the .\checks\Instance.Tests.ps1 file
We will add a Describe block for this check with a Tags parameter which has a unique tag for this check only, a group tag (if required) and the $filename
Describe "Instance MaxDop" -Tags MaxDopInstance, MaxDop, $filename {
}
We need to load the assertion function (NOTE - it is . space $PSCriptRoot)
Describe "Instance MaxDop" -Tags MaxDopInstance, MaxDop, $filename {
. $PSScriptRoot/../internal/assertions/Assert-InstanceMaxDop.ps1
}
and get the configuration items
Describe "Instance MaxDop" -Tags MaxDopInstance, MaxDop, $filename {
. $PSScriptRoot/../internal/assertions/Assert-InstanceMaxDop.ps1
$UseRecommended = Get-DbcConfigValue policy.instancemaxdop.userecommended
$MaxDop = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.maxdop
$ExcludeInstance = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.excludeinstance
}
and then we use @(Get-Instance).ForEach or @(Get-ComputerName).ForEach to loop through the instances or hosts that are configured at app.sqlinstance and app.computername or provided by the parameters SqlInstance or ComputerName
Describe "Instance MaxDop" -Tags MaxDopInstance, MaxDop, $filename {
. $PSScriptRoot/../internal/assertions/Assert-InstanceMaxDop.ps1
$UseRecommended = Get-DbcConfigValue policy.instancemaxdop.userecommended
$MaxDop = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.maxdop
$ExcludeInstance = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.excludeinstance
@(Get-Instance).ForEach{
Context "Testing Instance MaxDop Value on $psitem" {
}
}
}
We can set the test to skip for the excluded instances (this can be used for databases or users etc)
Describe "Instance MaxDop" -Tags MaxDopInstance, MaxDop, $filename {
. $PSScriptRoot/../internal/assertions/Assert-InstanceMaxDop.ps1
$UseRecommended = Get-DbcConfigValue policy.instancemaxdop.userecommended
$MaxDop = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.maxdop
$ExcludeInstance = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.excludeinstance
@(Get-Instance).ForEach{
if($psitem -in $ExcludeInstance){$Skip = $true}else{$skip = $false}
Context "Testing Instance MaxDop Value on $psitem" {
It "Instance Level MaxDop setting should be correct on $psitem" -Skip:$Skip {
}
}
}
}
and then call the function with the parameters in the It block
Describe "Instance MaxDop" -Tags MaxDopInstance, MaxDop, $filename {
. $PSScriptRoot/../internal/assertions/Assert-InstanceMaxDop.ps1
$UseRecommended = Get-DbcConfigValue policy.instancemaxdop.userecommended
$MaxDop = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.maxdop
$ExcludeInstance = Get-DbcConfigValue -Module dbachecks -Name policy.instancemaxdop.excludeinstance
@(Get-Instance).ForEach{
if ($psitem -in $ExcludeInstance) {$Skip = $true}else {$skip = $false}
Context "Testing Instance MaxDop Value on $psitem" {
It "Instance Level MaxDop setting should be correct on $psitem" -Skip:$Skip {
Assert-InstanceMaxDop -Instance $psitem -UseRecommended:$UseRecommended -MaxDopValue $MaxDop
}
}
}
}
The last thing to add is the description of the check. This needs to be added to .\internal\configurations\DbcCheckDescription.json. The description needs to start with "Tests" and if there are configuration values they should be refered to as "specified" and the default values mentioned
{
"UniqueTag": "MaxDopInstance",
"Description": "Tests that the instance level MaxDop settings on all instances (except those specified default blank) are set to the recommended value if specified (default false) or to the specified value (default 0)"
}
Once that has been added run the Unit tests as described in .\Running-Unit-Tests.md and then push your changes and create PR :-)
** THANK YOU **