In the first part of this post I discussed my thoughts on how I wanted to test stored procedures. The next step for me was to find a framework to run all this in! This post presents what I’ve found and written.
It was a pretty quick search in Google that brought my attention to the TSqlUnit project on sourceforge. There doesn’t seem much activity on this at the moment but it’s fairly self-contained with full source code available for customisation if necessary. I particularly like this framework because of its simplicity and the fact that it entirely SQL-based. Currently I like to write and test SQL in Microsoft Sql Server Management Studio – it might not have intellisense (unless you install the RedGate utility) but it’s a fast environment to work within and run SQL directly. TSqlUnit allows me to run one sproc to kick off one or more of my tests and maintains the xUnit style.
This is all well and good but I want to be able to integrate those tests with the automated build - so my plan is to call TSqlUnit from Visual Studio-based unit tests which can be triggered by the build. Because I’ve only got Visual Studio 2008 Standard Edition I couldn’t develop this with the VS test framework so I’ve used NUnit instead with Using statements that allow me to swap between the two xUnit frameworks later.
My plan is simple – during compilation of the Unit Test assembly in my project, generate code within it that calls each of the TSqlUnit test sprocs in the database. I could have written just one test method that called the TSqlUnit sproc to run all tests but this isn’t going to look very good when I run the tests in Visual Studio and one fails – I’m not going to be told which tests fails in the normal manner but rather the ‘run all tests’ failed.
Instead I’ve chosen to use the Text Templating built into VS 2008 to generate the code during the compilation. The text template has a .tt file extension and in VS uses a custom tool called TextTemplatingFileGenerator to take the contents of the template and compile it into the target language (C# in my case) placing the source into a code-behind file. The custom tool is triggered when I save the template and the resulting code gets compiled into the assembly during the normal build – it’s at this point where NUnit, which I run in the background already pointing at the assembly, sees the new assembly and automatically loads it and runs all the tests. Here’s a look at how my project appears in Visual Studios solution explorer :
This highlights the text template (SqlUnitTests.tt) and the code-behind which will be generated containing one Microsoft/NUnit test for each test sproc.
I’ve provided the text template code I was using below (sorry, it’s not syntax highlighted because I haven’t found the Windows Live Writer plugin yet ;) ) Currently it has the SQL Server instance and database names embedded in the code so it still needs some work to extract those to a config file. However, this represents very little code that can automatically generate a huge number of test cases with little or no further interaction.
The only other development I had to do was a change to TSqlUnit so that the tsu_runTests sproc could accept an optional test name and returns the error message if the test fails.
This is all very much work-in-progress (time allowing as ever) and I’m currently waiting to dip my toes into the unit testing features of Visual Studio Team Edition for Database Developers now that it’s more accessible and compare its features with my requirements before taking my solution any further. However, I liked the use of the text templates and the usefulness of TSqlUnit that I thought it worthwhile posting now.
<#@ template language="C#" debug="True" #>
<#@ assembly name="Microsoft.SqlServer.ConnectionInfo" #>
<#@ assembly name="Microsoft.SqlServer.Smo" #>
<#@ import namespace="Microsoft.SqlServer.Management.Smo" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#
// Use the following to kick off debugger
//System.Diagnostics.Debugger.Break();
#>
using System;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Configuration;
using NUnit.Framework;
namespace SqlUnitTestFramework
{
<#
Server server = new Server(@"(local)\SQLEXPRESS");
Database database = new Database(server, "Northwind");
database.StoredProcedures.Refresh();
Dictionary<String, List<String>> tests = new Dictionary<string, List<string>>();
String CategoryName;
String TestName;
foreach (StoredProcedure sproc in database.StoredProcedures)
{
if (sproc.Name.StartsWith("ut_"))
{
Match match = Regex.Match(sproc.Name, "ut_([^_]*)_?(.*)");
if (match.Groups[2].Value == "")
{
CategoryName = "NoCategory";
TestName = match.Groups[1].Value;
}
else
{
CategoryName = match.Groups[1].Value;
TestName = match.Groups[2].Value;
}
if (String.Compare(TestName, "setup", true)!=0
&& String.Compare(TestName, "teardown", true)!=0)
{
if (!tests.ContainsKey(CategoryName))
{
tests.Add(CategoryName, new List<String>());
}
tests[CategoryName].Add(TestName);
}
}
}
foreach (string category in tests.Keys)
{
PushIndent("\t");
WriteLine("[TestFixture]");
WriteLine("public class {0}Tests", category);
WriteLine("{");
foreach (string test in tests[category])
{
PushIndent("\t");
WriteLine("[Test]");
WriteLine("public void {0}()", test);
WriteLine("{");
PushIndent("\t");
WriteLine("Utility.ExecuteNamedTest(\"{0}\");", test);
PopIndent();
WriteLine("}");
PopIndent();
}
WriteLine("}");
PopIndent();
}
#>
public class Utility
{
public static void ExecuteNamedTest(string testName)
{
using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["NorthWind"].ConnectionString))
{
using (SqlCommand cmd = new SqlCommand())
{
cmd.Connection = conn;
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "tsu_runTests";
cmd.Parameters.Add("suite", SqlDbType.NVarChar, 255);
cmd.Parameters["suite"].Direction = ParameterDirection.InputOutput;
cmd.Parameters.Add("testName", SqlDbType.NVarChar, 255);
cmd.Parameters["testName"].Direction = ParameterDirection.InputOutput;
cmd.Parameters["testName"].Value = testName;
cmd.Parameters.Add("testError", SqlDbType.NVarChar, 255);
cmd.Parameters["testError"].Direction = ParameterDirection.Output;
cmd.Parameters.Add("success", SqlDbType.Bit);
cmd.Parameters["success"].Direction = ParameterDirection.Output;
cmd.Parameters.Add("testCount", SqlDbType.Int);
cmd.Parameters["testCount"].Direction = ParameterDirection.Output;
cmd.Parameters.Add("failureCount", SqlDbType.Int);
cmd.Parameters["failureCount"].Direction = ParameterDirection.Output;
cmd.Parameters.Add("errorCount", SqlDbType.Int);
cmd.Parameters["errorCount"].Direction = ParameterDirection.Output;
conn.Open();
cmd.ExecuteScalar();
if ((int)cmd.Parameters["failureCount"].Value > 0 || (int)cmd.Parameters["errorCount"].Value > 0)
{
Assert.Fail((string)cmd.Parameters["testError"].Value);
}
}
}
}
}
}