March 2008 - Posts

How To: Unit Test a T-SQL Trigger with VSTS Database Edition

In Visual Studio Team System Database Edition, three types of functionality are unit tested; Stored Procedures, Functions, and Triggers.  It’s not so obvious how to test a trigger and at this time there are not many examples.  This is probably because there are so many different testing needs.  Testing the trigger is very similar to the more obvious calls to stored procedures and functions.  This example shows how a trigger can be unit tested using the RAISERROR method to cause a test harness failure.  Two other methods exist to perform unit testing failures for those complicated tests. 

1.       The Assert classes that we can use in the code-behind test classes, or

2.       Test conditions in the Database Unit Test Designer.  These are Microsoft provided test conditions or custom written test conditions that check the test results and either pass or fail.

In the following example, we simply count the number of records before and after the table action that causes a trigger.  We can now get the Red-Yellow-Green lights for TDD.  We might want to split this into three different tests and add transaction support.  Transaction support is added in either the T-SQL unit test as listed below or we can add it in the Visual Studio C# or VB.NET generated test code.


The T-SQL Unit Test

-- Database unit test for dbo.trg_CopyToHistoricalPrimaryRecords

--

DECLARE @beforeCount INT,

      @afterCount INT,

      @PrimaryField1 INT,

      @PrimaryField2 UNIQUEIDENTIFIER,

      @PrimaryField3 BIT,

      @PrimaryTableId INT

 

SELECT @PrimaryField1 = 0,

      @PrimaryField2 = NULL,

      @PrimaryField3 = 0

 

--  INSERT Trigger Test

SET @beforeCount = (SELECT COUNT(*) FROM HistoricalPrimaryRecords);

 

INSERT INTO [dbo].[PrimaryTable] ( [PrimaryField1], [PrimaryField2], [PrimaryField3] )

      VALUES( @PrimaryField1, @PrimaryField2, @PrimaryField3 );

 

SET @PrimaryTableId = SCOPE_IDENTITY();

 

SET @afterCount = (SELECT COUNT(*) FROM HistoricalPrimaryRecords);

 

IF NOT @afterCount = @beforeCount + 1

    RAISERROR ('TRIGGER INSERT ERROR: trg_CopyToHistoricalPrimaryRecords did not insert a record in the historical table.', 1, 1);

 

---  UPDATE Trigger Test

SET @beforeCount = (SELECT COUNT(*) FROM HistoricalPrimaryRecords);

 

UPDATE [dbo].[PrimaryTable]

      SET

            [PrimaryField1] = @PrimaryTableId

      WHERE

            [PrimaryTableId] = @PrimaryTableId

 

SET @afterCount = (SELECT COUNT(*) FROM HistoricalPrimaryRecords);

 

IF NOT @afterCount = @beforeCount + 1

    RAISERROR ('TRIGGER UPDATE ERROR: trg_CopyToHistoricalPrimaryRecords did not insert a record in the historical table.', 1, 1);

 

---  DELETE Trigger Test

SET @beforeCount = (SELECT COUNT(*) FROM HistoricalPrimaryRecords);

 

DELETE FROM [dbo].[PrimaryTable]

       WHERE [PrimaryTableId] = @PrimaryTableId

 

SET @afterCount = (SELECT COUNT(*) FROM HistoricalPrimaryRecords);

 

IF NOT @afterCount = @beforeCount + 1

    RAISERROR ('TRIGGER DELETE ERROR: trg_CopyToHistoricalPrimaryRecords did not insert a record in the historical table.', 1, 1);

 

-- PRINT '@beforeCount: ' + Cast(@beforeCount as varchar(10));

-- PRINT '@afterCount: ' + Cast(@afterCount as varchar(10));

 

The Trigger

ALTER TRIGGER [dbo].[trg_CopyToHistoricalPrimaryRecords]

   ON [dbo].[PrimaryTable]

   AFTER INSERT,DELETE,UPDATE

AS

BEGIN

      SET NOCOUNT ON;

 

      -- UPDATE, DELETE: Capture all changes that occurred from an Insert and Delete.

      INSERT INTO [PrimaryForeignKeys].[dbo].[HistoricalPrimaryRecords]

           ( PrimaryTableId

           , PrimaryField1

           , PrimaryField2

           , PrimaryField3)

            (SELECT d.PrimaryTableId

           , d.PrimaryField1

           , d.PrimaryField2

           , d.PrimaryField3

             FROM deleted d);

 

      --  INSERT: Capture all newly inserted records that were not from Inserts and Deletes.

      IF (0 = (SELECT COUNT(*) FROM deleted))

      BEGIN

      INSERT INTO [PrimaryForeignKeys].[dbo].[HistoricalPrimaryRecords]

           ( PrimaryTableId

           , PrimaryField1

           , PrimaryField2

           , PrimaryField3)

            (SELECT i.PrimaryTableId

           , i.PrimaryField1

           , i.PrimaryField2

           , i.PrimaryField3

             FROM inserted i);

      END
END

 

Posted by vblasberg with no comments
Filed under: ,

The hierarchyid Data Type

Last night’s North Dallas .Net User Group meeting was really good. Peter Debetta presented new features on SQL 2008. All of it was really interesting and will eventually be useful. I wouldn’t want to rehash all of the Microsoft documents and other blog information, but I will say that there are some useful features on the way.

The one thing that I appreciated but felt a little pang of irritation was the new “hierarchyId” data type. I spent many a night trying to get this perfect in a past project as did Peter. This data type holds the relationship of the contained data in relation to its parent with easy access and update to Ancestors and Descendents. The one downside is that this new data type only manages trees of data and not a recursive matrix.

I’ll jump on this feature next time I have to utilize an optimized tree of data using stored procedures. The other memorable feature is the Merge function that makes synchronizing records more natural.  It's a bit limiting but it works.

So SQL 2008 is scheduled for release third quarter, but we know that companies are slow to adopt, even with all of the great features and the huge beta testers out there. At least sometimes we can start projects with the express versions and use these features to prove the product before production databases get converted.

We can read more about the hierarchyid data type at:
http://msdn2.microsoft.com/en-us/library/bb677290(SQL.100).aspx

-Vince

Posted by vblasberg with no comments
Filed under: ,
More Posts