I hope it is no secret that testing of written code is as important as writing the code itself, and sometimes even more important. Writing unit test for C#/Java/…code coverage is the responsibility of each software developer.
However, there is not always an opportunity to use autotests. For example, until recently, there were no good computer-aided testing systems for SQL Server, and many people had to create self-made products that were expensive to support and develop. To tell the truth, I was doing that too.
In 2014, I first discovered tSQLt, which turned to be a very nice free open-source framework for unit testing. In this post, I will try to show you how tSQLt can greatly simplify your life.
Oftentimes, I have to audit servers to identify non-optimal configuration settings.
Usually, it looks like this. I run a bunch of scripts on the server and manually analyze the results of the samples. Let’s try to automate this.
First, we need to download the latest version of tSQLt. Next, we should configure the instance of SQL Server to work with CLR and create a database, on which the framework will be installed:
EXEC sys.sp_configure 'clr enabled', 1
RECONFIGURE
GO
USE [master]
GO
IF DB_ID('tSQLt') IS NOT NULL BEGIN
ALTER DATABASE tSQLt SET SINGLE_USER WITH ROLLBACK IMMEDIATE
DROP DATABASE tSQLt
END
GO
CREATE DATABASE tSQLt
GO
USE tSQLt
GO
ALTER DATABASE tSQLt SET TRUSTWORTHY ON
GO
Then, run tSQLt-based script file tSQLt.class.sql from the archive. The script will create its own tSQLt scheme, the CLR assembly and a plurality of scripted objects. Some procedures will have prefix Private_, which are intended for internal use by the framework.
Upon successful installation, we will get the following message in the Output:
+-----------------------------------------+
| Thank you for using tSQLt. |
| tSQLt Version: 1.0.5873.27393 |
+-----------------------------------------+
Now, let’s create a scheme, in which autotests will be created:
USE tSQLt
GO
CREATE SCHEMA [Server] AUTHORIZATION dbo
GO
EXEC sys.sp_addextendedproperty @name = N'tSQLt.TestClass'
, @value = 1
, @level0type = N'SCHEMA'
, @level0name = N'Server'
GO
Please note that the extended property defines a particular object belonging to tSQLt functionality.
Let’s create a test in the Server scheme, make sure to indicate prefix test– in the name of the test:
CREATE PROCEDURE [Server].[test MyFirstAutoTest]
AS BEGIN
SET NOCOUNT ON;
EXEC tSQLt.Fail 'TODO: Implement this test.'
END
Execute the created autotest. We can either execute:
EXEC tSQLt.RunAll
or explicitly specify the schema:
EXEC tSQLt.Run 'Server'
or a specific test:
EXEC tSQLt.Run 'Server.test MyFirstAutoTest'
If you need to run the last test executed, you can call Run without the parameters:
EXEC tSQLt.Run
After executing one of the commands above, we will get the following information:
[Server].[test MyFirstAutoTest] failed: (Failure) TODO: Implement this test.
+----------------------+
|Test Execution Summary|
+----------------------+
|No|Test Case Name |Dur(ms)|Result |
+--+-------------------------------+-------+-------+
|1 |[Server].[test MyFirstAutoTest]| 0|Failure|
Let’s create an autotest with a more useful content. For example, we can check which databases have never been backed up. Or go even further… and learn which databases in FULL RECOVERY require BACKUP LOG.
Why is it an essential problem that should be checked automatically?
I mentioned not once that you should always make backups. But BACKUP LOG is worth mentioning as a separate issue. After the first FULL BACKUP to the database in FULL RECOVERY, the log file is no longer cleared and due to the lack of free space in the file, it will gradually grow until free space expires on the disk or until we execute BACKUP LOG.
USE [tSQLt]
GO
CREATE PROCEDURE [Server].[test CheckBackup]
AS BEGIN
SET NOCOUNT ON;
DECLARE @SQL NVARCHAR(MAX)
SELECT @SQL = (
SELECT '
' + QUOTENAME(d.name) + ': ' +
CASE
WHEN t.database_id IS NULL OR t.full_backup = 0
THEN 'NO FULL BACKUP'
WHEN d.recovery_model IN (0,1) AND log_backup = 0
THEN 'NO LOG BACKUP'
WHEN DATEDIFF(DAY, t.last_full_backup, GETDATE()) > 7
THEN 'FULL BACKUP IS OUTDATED (LAST BACKUP: '
+ CONVERT(NVARCHAR(MAX), t.last_full_backup, 120) + ')'
END
FROM sys.databases d
LEFT JOIN (
SELECT
database_id = DB_ID(s.database_name)
, last_full_backup = MAX(CASE WHEN s.[type] = 'D' THEN s.backup_finish_date END)
, full_backup = COUNT(CASE WHEN s.[type] = 'D' THEN 1 END)
, log_backup = COUNT(CASE WHEN s.[type] = 'L' THEN 1 END)
FROM msdb.dbo.backupset s
WHERE s.[type] IN ('D', 'L')
GROUP BY s.database_name
) t ON t.database_id = d.database_id
WHERE d.name NOT IN ('tempdb')
AND (
t.database_id IS NULL
OR
DATEDIFF(DAY, t.last_full_backup, GETDATE()) > 7
OR
t.full_backup = 0
OR
(d.recovery_model IN (0,1) AND log_backup = 0)
)
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')
IF @SQL IS NOT NULL
EXEC tSQLt.Fail @SQL
END
Let’s execute this:
EXEC tSQLt.Run 'Server.test CheckBackup'
[Server].[test CheckBackup] failed: (Failure)
[master]: NO FULL BACKUP
[model]: NO FULL BACKUP
[msdb]: NO FULL BACKUP
[WideWorldImporters]: FULL BACKUP IS OUTDATED (LAST BACKUP: 2016-06-01 14:48:29)
[tSQLt]: NO LOG BACKUP
+----------------------+
|Test Execution Summary|
+----------------------+
|No|Test Case Name |Dur(ms)|Result |
+--+---------------------------+-------+-------+
|1 |[Server].[test CheckBackup]| 27|Failure|
Let’s execute the required operations on creation of backups to check the correctness of the test:
+----------------------+
|Test Execution Summary|
+----------------------+
|No|Test Case Name |Dur(ms)|Result |
+--+---------------------------+-------+-------+
|1 |[Server].[test CheckBackup]| 10|Success|
Let’s create a couple of useful tests. For example, for keeping track of how many AutoGrow events occur on the server.
I hope it’s no secret that the execution of any transaction requires a certain space on the disk, in the data file or log. In general, if there is not enough space, then the file automatically grows. At this point, the file is blocked and SQL Server will wait for the disk subsystem to make the necessary operations on allocation of space on the disk.
By default, SQL Server zero-initializes new space on the disk. This behavior can be disabled for data files through the use of Instant File Initialization, and the time for allocating disk space can be reduced. But initialization will still happen for log files, and this is definitely slow. Therefore, it is recommended to keep track of Auto Grow events on a regular basis:
USE [tSQLt]
GO
CREATE PROCEDURE [Server].[test CheckAutoGrow]
AS BEGIN
SET NOCOUNT ON;
DECLARE @SQL NVARCHAR(MAX)
SELECT @SQL = (
SELECT '
' + QUOTENAME(DB_NAME(DatabaseID)) + ': ' + CAST(COUNT(1) AS NVARCHAR(10))
+ ' events in ' + [FileName]
+ ' (total waits: ' + CAST(SUM(Duration) / 1000 AS NVARCHAR(10))
+ 'ms)'
FROM sys.traces i
CROSS APPLY sys.fn_trace_gettable([path], DEFAULT) t
WHERE t.EventClass IN (92, 93)
AND i.is_default = 1
GROUP BY DatabaseID, [FileName]
HAVING COUNT(1) > 2
OR SUM(Duration) / 1000 > 300
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')
IF @SQL IS NOT NULL
EXEC tSQLt.Fail @SQL
END
[Server].[test CheckAutoGrow] failed: (Failure)
[tempdb]: 67 events in templog (total waits: 2492ms)
[tempdb]: 112 events in tempdev (total waits: 8149ms)
[tSQLt]: 1 events in tSQLt_log (total waits: 346ms)
[tSQLt]: 3 events in tSQLt (total waits: 283ms)
+----------------------+
|Test Execution Summary|
+----------------------+
|No|Test Case Name |Dur(ms)|Result |
+--+-----------------------------+-------+-------+
|1 |[Server].[test CheckBackup] | 10|Success|
|2 |[Server].[test CheckAutoGrow]| 26|Failure|
Now let’s add one more test, which will check whether Instant File Initialization I’ve mentioned earlier is enabled on the server:
USE [tSQLt]
GO
CREATE PROCEDURE [Server].[test InstantFileInitializationEnabled]
AS BEGIN
SET NOCOUNT ON;
DECLARE @IsEnabled BIT
IF EXISTS (
SELECT 1
FROM sys.configurations
WHERE name = 'xp_cmdshell'
AND value_in_use = 1
AND IS_SRVROLEMEMBER('sysadmin') = 1
) BEGIN
DECLARE @temp TABLE (Value VARCHAR(8000))
INSERT INTO @temp
EXEC sys.xp_cmdshell 'whoami /priv'
SELECT @IsEnabled =
CASE WHEN Value LIKE '%Enabled%' COLLATE SQL_Latin1_General_CP1_CI_AS
THEN 1
ELSE 0
END
FROM @temp
WHERE Value LIKE '%SeManageVolumePrivilege%'
END
IF ISNULL(@IsEnabled, 0) = 0
EXEC tSQLt.Fail 'Instant File Initialization NOT ENABLED'
END
Thus, we already have three autotests, which are stored in tSQLt. If we need to conduct an audit and verify the correctness of settings, all we have to do is to run autotests. And to check another server, we need to backup the tSQLt database and deploy it at a new location.
What else should be mentioned? Don’t forget that tSQLt turns each test you run into a transaction. Therefore, if your stored procedure uses its own transactions – it should be done cautiously. For example, the test of this procedure will fail:
CREATE PROC TEST
AS BEGIN
BEGIN TRAN TR
BEGIN TRY
SELECT 1 / 0
COMMIT TRAN TR
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRAN TR
END CATCH
END
Although the procedure works without errors outside the test. The reason for the problem is that ROLLBACK in the procedure rolls back not only your transaction, but also the tSQLt transaction and the number of active transactions will change on return.
And now a small dessert…
For those who like GUI, green and red checkmarks in front of tests, etc., the Devart company has developed dbForge Unit Test for SQL Server – a powerful tSQLt-based plug-in for SSMS that allows to automate all the work with Unit-tests.
All that we have done using scripts, you can easily create and edit with the help of GUI:
You can also run tests and analyze the results of execution:
At this point, I finish telling you about the capabilies of unit testing in SQL Server. I hope this information will be helpful.
For those who are interested in trying out dbForge Unit Test for SQL Server – you can download it here.
Reference: Pinal Dave (http://blog.sqlauthority.com)
First appeared on Creating and Running an SQL Server Unit Test – Best Ways to Test SQL Queries