Skip to content

Writing a New Check

Rob Sewell edited this page May 22, 2018 · 2 revisions

Writing a New Check

Fork the Repo and create new branch from the development branch

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)

Reasoning

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

Consider exactly what needs to be checked

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

Create some configuration items

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.

Function for our test

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

image

Write some Unit Tests for our function

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

Little Trick

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

image

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.

Add the test to the correct file

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
            }
        }
    }
}

Description

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 **