Automating tSQLt Tests

Automating tSQLt Unit Tests

This is the ninth part in the series: SQL Server and Continuous Integration. In this post I’ll add some unit tests and pull them into the build process in GitLab. Topics covered so far in this series are:

To have confidence in our database changes, we need to run tests to make sure the database still operates as expected. I’ll be using the opensource tSQLt unit testing framework for T-SQL. I’ll only be covering the automation of tSQLt tests, not the actual writing of tests. The user guide is a good starting point for getting to grips with tSQLt.

It’s possible to add a secondary ReadyRoll project in which you can develop your tSQLt tests, see here. This allows you to execute your tests in Visual Studio with a nice visual output. I prefer to develop (and run my tests) in SSMS, switching to VS every time I want to run some tests isn’t ideal.

I use a PowerShell script in the build to execute my tests. In the future, I can have the script run coverage reports (which I’ll cover in my next post in the series) , integration tests, ostress, Pester tests and much more.

tSQLt

Before we can write any tests, we need to install the tSQLt framework in our development database.

  • Enable CLR on each SQL instance you will be running tests on
  • Download the framework from tSQLt.org
  • Run the tSQLt.class.sql script against your development database

Now we’re ready to test the function below:

CREATE FUNCTION dbo.ConvertCurrency 
(
    @rate DECIMAL(10,4), 
    @amount DECIMAL(10,4)
)
RETURNS DECIMAL(10,4)
AS
BEGIN
    DECLARE @Result DECIMAL(10,4);
 
    SET @Result = (SELECT @amount / @rate);
 
    RETURN @Result;
END;

First, I create a test class.

EXEC tSQLt.NewTestClass 'ConvCurrency';

Then, this simple test:

CREATE PROCEDURE ConvCurrency.[test CC converts correctly]
AS
BEGIN
    DECLARE @actual DECIMAL(10,4), 
            @rate DECIMAL(10,4) = 1.2,
            @amount DECIMAL(10,4) = 2.00;
 
    SELECT @actual = dbo.ConvertCurrency(@rate, @amount);
 
    DECLARE @expected DECIMAL(10,4) = 2.4;  
 
    EXEC tSQLt.AssertEquals @expected, @actual;
END;

When I want to run the test, I execute tSQLt.RunAll in SSMS and receive these results:

[ConvCurrency].[test CC converts correctly] failed: (Failure) Expected: <2.4000> but was: <1.6667>
 
+----------------------+
|Test Execution Summary|
+----------------------+
 
|No|Test Case Name                             |Dur(ms)|Result |
+--+-------------------------------------------+-------+-------+
|1 |[ConvCurrency].[test CC converts correctly]|     50|Failure|
-----------------------------------------------------------------------------
Msg 50000, Level 16, State 10, Line 1
Test Case Summary: 1 test case(s) executed, 0 succeeded, 1 failed, 0 errored.
-----------------------------------------------------------------------------

Correcting the function (to multiply instead of divide) and rerunning the test shows that we are now in a good state.

+----------------------+
|Test Execution Summary|
+----------------------+
 
|No|Test Case Name                             |Dur(ms)|Result |
+--+-------------------------------------------+-------+-------+
|1 |[ConvCurrency].[test CC converts correctly]|      7|Success|
-----------------------------------------------------------------------------
Test Case Summary: 1 test case(s) executed, 1 succeeded, 0 failed, 0 errored.
-----------------------------------------------------------------------------

By default, ReadyRoll will ignore tSQLt objects, including our tests. We don’t want ReadyRoll to script out the tSQLt objects, but we do want it to script our tests. To set our filter we need to unload the project in VS and edit the project file. Add the following to the section named ReadyRoll Script Generation Section:

<SyncOptionIgnoretSQLt>False</SyncOptionIgnoretSQLt>

<ExcludeObjectsFromImport>
  Assembly=\[tSQLtCLR\];
  Schema=\[tSQLt\];
  StoredProcedure=\[tSQLt\]\.\[(.*?)\];
  Table=\[tSQLt\]\.\[(.*?)\];
  UserDefinedDataType=\[tSQLt\]\.\[(.*?)\];
  View=\[tSQLt\]\.\[(.*?)\];
  UserDefinedFunction=\[tSQLt\]\.\[(.*?)\];
</ExcludeObjectsFromImport>

ReadyRoll

We can then refresh the ReadyRoll project to sync our new function, schema (test class) and test.

tSQLt and ReadyRoll

tSQLt tests synced to ReadyRoll project

Our tests will now be included in our Git repo, but we don’t want to deploy them to production. We need to add a SQLCMD variable to the deployment package, like we did in the post Starting a ReadyRoll Project. We will then be able to decide if we want to deploy our tests or not.

Add a SQLCMD variable named $(DeployUnitTestObjects) with a default value of False. Add the following line to the top of each unit test file in the ReadyRoll project:

-- <Migration Condition="'$(DeployUnitTestObjects)' = 'True'" />

When we run the deployment package into production, we can now specify to not deploy the unit tests.

Automation

To get these tests to run automatically on each build, we need to add the tSQLt.class.sql script to our solution. Copy the script to the root of the solution. Then right click the solution, select “Add Existing Item” and select the script.

Add tSQLt installer to solution

Now we can create the PowerShell script to run the tests. The script below should be created in the root of the solution and added as an existing item.

param(
    [Parameter(Mandatory=$true)][string]$OutputFolder,
    [Parameter(Mandatory=$true)][string]$DatabaseServer,
    [Parameter(Mandatory=$true)][string]$DatabaseName
  )

$Query = @"
SELECT value_in_use  
FROM sys.configurations 
WHERE name = 'clr enabled';
"@ 

$CLREnabled = Invoke-Sqlcmd `
    -Query $Query `
    -ServerInstance $DatabaseServer `
    -Database $DatabaseName 
    
if($CLREnabled.value_in_use -eq 1){
    Invoke-Sqlcmd `
       -InputFile "$PSScriptRoot\tSQLt.class.sql" `
       -ServerInstance $DatabaseServer `
       -Database $DatabaseName 

    Sqlcmd `
        -Q "EXEC tSQLt.RUNALL;" `
        -S $DatabaseServer `
        -d $DatabaseName `
        -o "$OutputFolder\$DatabaseName tSQLt Report.txt"
} 
else {     
    Write-Error("CLR is not enabled on $DatabaseServer.")
}

GitLab

We need to tell GitLab to run our test harness. We give instructions to GitLab via the .gitlab-ci.yml file (see the last post in this series for details).

build:
  script: '"C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe" ^
  /p:TargetServer=LocalHost ^
  /p:TargetDatabase=RR_Test ^
  /p:Configuration=Release ^
  /p:GenerateSqlPackage=True ^
  /p:DBDeployOnBuild=True'

test:
  script: powershell .\TestHarness.ps1 %CI_PROJECT_DIR% 'LocalHost' 'RR_Test'
  artifacts:
    paths: 
    - RR_Test
    untracked: true

The build section deploys our database project to the default instance on the runner. The last part tells GitLab to save any artifacts(files) that are generated during the test phase of the build. Our test harness is writing the results of our tSQLt tests out to the file system, so they will be stored against the build as artifacts.

Once we commit and push to GitLab you will see the new test step in the pipeline. If we go into the test section of the build, we can browse or download the build artifacts.

tSQLt tests automated in GitLab

We finally have a basic CI pipeline, with automated deployment to a test environment, test execution and result capture. In the next post, I will add some more tests and a coverage report to show which parts of the database we are (and aren’t) testing.

About the author

James Anderson

I'm a freelance SQL Server DBA in Southampton, UK. Performance tuning code and improving development processes are what I love to do for work. Outside of work, I live to travel to as many places as possible with my wife Sarah.

One thought on “Automating tSQLt Unit Tests”

Leave a Reply

Your email address will not be published. Required fields are marked *

Sign Up

James Anderson SQL Server DBA

Hi, I'm James Anderson. When I'm not blogging about SQL Server, I do this.

The SQL People

DBA Stackexchange Profile