duminică, 27 februarie 2011

Unit Testing TSQL in SQL Server 2005and the tSQLT Framework–part 1

 

Installing tSQLT

Installing tSQLT is pretty simple. You can find the .zip file containing two SQL script files here http://sourceforge.net/projects/tsqlt/ . After downloading the zip and extracting it, run the two .sql files on your desired database in this order: SetClrEnabled.sql, tSQLt.class.sql. That’s it, tSQLT has been installed.

There is also a tutorial here: http://www2.sqlity.net/articles/tSQLtTutorial and an user guide here: http://www2.sqlity.net/tsqlt/tsqlt-user-guide 

After running tSQLt.class.sql, you may get the following warnings:

Compiled at 2011-02-27 12:57:27.803
The module 'DefaultResultFormatter' depends on the missing object 'tSQLt.TableToText'. The module will still be created; however, it cannot run successfully until the object exists.
The module 'RunTestClass' depends on the missing object 'tSQLt.Run'. The module will still be created; however, it cannot run successfully until the object exists.
The module 'Run' depends on the missing object 'tSQLt.private_RunTestClass'. The module will still be created; however, it cannot run successfully until the object exists.
The module 'SetFakeViewOff' depends on the missing object 'tSQLt.private_SetFakeViewOff_SingleView'. The module will still be created; however, it cannot run successfully until the object exists.


You can ignore them for now and take a look at your database, and under  stored procedures you should see a whole bunch using the tsql schema:

tsql_storedprocs

tSQLt created those, along with a view, some tables, table-valued functions, scalar-valued functions and other objects, including a CLR assembly.

Testing with tSQLt

Let’s start with a simple test. Testing a scalar-valued function that returns the last working day of previous month.

Here is the function we want to test:

CREATE FUNCTION [dbo].[fnGetLastWorkingDayOfPreviousMonth] 
(
-- Add the parameters for the function here

)
RETURNS datetime
AS
BEGIN
declare @lastdayofpreviousmonth datetime
declare @lastworkingdayofpreviousmonth datetime
if(@@DATEFIRST=7)
begin
select @lastdayofpreviousmonth = DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE()),0))
if(DATEPART(dw, @lastdayofpreviousmonth) between 2 and 6)
set @lastworkingdayofpreviousmonth= @lastdayofpreviousmonth
else
set @lastworkingdayofpreviousmonth = DATEADD(day, -(1+DATEPART(dw, @lastdayofpreviousmonth)%@@DATEFIRST), @lastdayofpreviousmonth)
end
return @lastworkingdayofpreviousmonth

END

First thing to do is set up a test class. This is essentially a schema we assign to all our tests. Using this class, we can run all tests inside it, or choose to run them selectively.

EXEC tSQLt.NewTestClass 'DemoTSQLT';
GO

Creating a test for our function is as easy as creating a stored procedure having the schema 'DemoTSQLT'.  tSQLt provides a nice way to test if two values are equal : tSQLt.assertEquals. The test for our function would look like this:

Create procedure [DemoTSQLT].[test last working day in previous month]
AS
BEGIN
declare @expectedResult datetime
set @expectedResult = cast('01-31-2011' as datetime)
declare @actualResult datetime
select @actualResult = dbo.fnGetLastWorkingDayOfPreviousMonth() 

exec tSQLt.assertEquals @expectedResult, @actualResult
END
GO

Because the function to test was designed to include the time portion of the date, effectively making ‘2011-01-31 23:59:59.000’ the last working day of previous month, this test will fail spectacularly. To run the test and see it fail, we can simply do the following:

tsqlt.Run 'DemoTSQLT'
GO

The output of this test can be seen bellow:

outputasserteqaulsfailed

Here, we ran all tests in  ‘DemoTSQLT ‘ class. To run a specific test, change tsqlt.Run to tsqlt.RunTest like this:

tsqlt.RunTest '[DemoTSQLT].[test last working day in previous month]'
GO

Let’s change the test a bit: Where we cast ‘01-31-2011’  as datetime, let’s change that to ‘2011-01-31 23:59:59.000'. Running the test (tsqlt.Run ‘DemoTSQLT’) now will have this result:

+---------------------+
|Test Execution Sumary|
+---------------------+
 
|No|Test Case Name                                       |Result |
+--+-----------------------------------------------------+-------+
|1 |[DemoTSQLT].[test last working day in previous month]|Success|
-----------------------------------------------------------------------------
Test Case Summary: 1 test case(s) executed, 1 succeeded, 0 failed, 0 errored.
-----------------------------------------------------------------------------

Test succeeded, developer happy, function is ready to be used.

There you have it. There is a lot more to tSQLt to be sure and I will try to cover it as much as possible in the next parts of the series. Have fun and happy testing.

Cheers,
Radu

duminică, 13 februarie 2011

Razor, ASP.NET MVC3 and a quick funny little app

I’ve been trying lately to keep up with Microsoft and the .NET framework but it’s been a loosing battle since the get go. The pace of which Microsoft churns out new features for .NET is amazingly scary if you are not some uber-programmer. So I’ve settled for just some areas, gave up my dreams of learning WPF and Silverlight and now I am focused on ASP.NET MVC (3).

I also have a problem with time management so, to kill  two birds with one stone, here is a little web app that keeps a time log of your actions. The idea is simple: When ever you start doing something that falls into a category or another, simply click the start button for the appropriate category and when you stop… yes, click the stop button. The difference in time (seconds, or minutes, or whatever you choose) is stored in a database, along with the date and time when you completed the action. Here is a little screen:

TimeLogScreenShot

So what do we need?

  1. ASP.NET MVC3
  2. JQuery Timers. I got mine from here: http://plugins.jquery.com/project/timers
  3. (Optional). The excellent CSS3 GitHub-like buttons from http://css3buttons.michaelhenriksen.dk/

Let’s begin. First create a new ASP.NET MVC3 Internet Application and choose Razor as your view engine. Razor is so cool, but totally different from the traditional aspx view engine. You will see why when you play a bit with it.

Create a new database with just two tables in it:

TimeLogDBSchema 

And here is the script for creating the two tables:

USE [TimeLog]
GO
IF EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_Duration]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_Duration]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [DF_LogEntry_Duration]
END


End
GO
IF EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_CreatedOn]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_CreatedOn]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [DF_LogEntry_CreatedOn]
END


End
GO
/****** Object: ForeignKey [FK_LogEntry_Category] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [FK_LogEntry_Category]
GO
/****** Object: Table [dbo].[LogEntry] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LogEntry]') AND type in (N'U'))
DROP TABLE [dbo].[LogEntry]
GO
/****** Object: Table [dbo].[Category] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Category]') AND type in (N'U'))
DROP TABLE [dbo].[Category]
GO
/****** Object: Table [dbo].[Category] Script Date: 02/13/2011 09:15:48 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Category]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Category](
[CategoryId] [int] IDENTITY(1,1) NOT NULL,
[CategoryName] [varchar](100) NULL,
CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED
(
[CategoryId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
SET ANSI_PADDING OFF
GO
/****** Object: Table [dbo].[LogEntry] Script Date: 02/13/2011 09:15:48 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LogEntry]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[LogEntry](
[LogId] [int] IDENTITY(1,1) NOT NULL,
[CategoryId] [int] NULL,
[Duration] [decimal](8, 2) NULL,
[CreatedOn] [datetime] NULL,
CONSTRAINT [PK_LogEntry] PRIMARY KEY CLUSTERED
(
[LogId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
/****** Object: Default [DF_LogEntry_Duration] Script Date: 02/13/2011 09:15:48 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_Duration]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_Duration]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] ADD CONSTRAINT [DF_LogEntry_Duration] DEFAULT ((0)) FOR [Duration]
END


End
GO
/****** Object: Default [DF_LogEntry_CreatedOn] Script Date: 02/13/2011 09:15:48 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_CreatedOn]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_CreatedOn]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] ADD CONSTRAINT [DF_LogEntry_CreatedOn] DEFAULT (getdate()) FOR [CreatedOn]
END


End
GO
/****** Object: ForeignKey [FK_LogEntry_Category] Script Date: 02/13/2011 09:15:48 ******/
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] WITH CHECK ADD CONSTRAINT [FK_LogEntry_Category] FOREIGN KEY([CategoryId])
REFERENCES [dbo].[Category] ([CategoryId])
GO
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] CHECK CONSTRAINT [FK_LogEntry_Category]
GO


Now, go to the Models folder in your MVC app and create a new Entity Framework model based on the database you’ve just created. And that takes care of the back-end.
 
Time for some controller action. There is only one method in your home controller you need to create:
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult InsertTimeLog(int categoryId, int value)
{
LogEntry le = new LogEntry();
Category cat = entities.Categories.First<Category>(c => c.CategoryId == categoryId);
le.Category = cat;
le.Duration = value;
le.CreatedOn = DateTime.Now;
entities.AddToLogEntries(le);
entities.SaveChanges();
return null;
}

You also need to change a bit the Index action to return all the categories in the database. Your home controller should look like this by now:
TimeLogEntities entities = new TimeLogEntities();
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
List<Category> categories = entities.Categories.ToList<Category>();
return View(categories);
}


[AcceptVerbs(HttpVerbs.Post)]
public JsonResult InsertTimeLog(int categoryId, int value)
{
LogEntry le = new LogEntry();
Category cat = entities.Categories.First<Category>(c => c.CategoryId == categoryId);
le.Category = cat;
le.Duration = value;
le.CreatedOn = DateTime.Now;
entities.AddToLogEntries(le);
entities.SaveChanges();
return null;
}

public ActionResult About()
{
return View();
}
}

Delete the default Index View in the Views/Home Folder and create a new one based on the above controller.

Ok, one more step to go: The index view. This is where I learnt to love Razor… The index view should look like this (explanations below the code):

@model IEnumerable<MvcApplication3.Models.Category>

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
<span>Categories</span>
</p>
<div class = "categories">
<ul id="CategoryList">

@foreach (var item in Model) {
<li>
<div class="@item.CategoryName.Replace(" ", "")">
<p>
<span class="Category" >@item.CategoryName</span>
<span style="padding-left:20px"></span>
<a class="big button" id="@String.Concat(@item.CategoryName.Replace(" ", ""),"Start")">Start</a>
<span style="padding-left:20px"></span>
<a style="padding-left:30px;padding-right:30px" class="big negative button" id="@String.Concat(@item.CategoryName.Replace(" ", ""),"Stop")" >Stop</a>
<span id="@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))" style="padding-left:20px">0</span>
</p>
</div>

</li>
}

</ul>
</div>
@foreach (var item in Model)
{
<script type="text/javascript">
   1:  
   2:  var categories = $("div.categories");
   3:      var @String.Concat("active", @item.CategoryName.Replace(" ", "")) = false;
   4:      $('.@item.CategoryName.Replace(" ", "")', categories).find('#@String.Concat(@item.CategoryName.Replace(" ", ""),"Start")').css("cursor", "pointer").click(function (i) {
   5:          if (!@String.Concat("active", @item.CategoryName.Replace(" ", ""))) {
   6:              $("#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))").text(0);
   7:              @String.Concat("active", @item.CategoryName.Replace(" ", "")) = !@String.Concat("active", @item.CategoryName.Replace(" ", ""));
   8:              $(this).parents("div").find('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').everyTime(1000, '@String.Concat("controlled", @item.CategoryName.Replace(" ", ""))', function (i) {
   9:                  $("#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))").text(i);
  10:              });
  11:          }
  12:      }).end().find('#@String.Concat(@item.CategoryName.Replace(" ", ""),"Stop")').css("cursor", "pointer").click(function (i) {
  13:          if (@String.Concat("active", @item.CategoryName.Replace(" ", ""))) {
  14:              @String.Concat("active", @item.CategoryName.Replace(" ", "")) = !@String.Concat("active", @item.CategoryName.Replace(" ", ""));
  15:              $(this).parents("div").find('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').stopTime('@String.Concat("controlled", @item.CategoryName.Replace(" ", ""))');
  16:              $.ajax({
  17:                  url: "/Home/InsertTimeLog/", type: "POST", dataType: "json",
  18:                  data: { categoryId: @item.CategoryId, value: $('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').text() },
  19:                  success: function () {
  20:                    ;;
  21:                  }
  22:              })
  23:          }
  24:      });
  25:   
</script>
}

Basically, what this view does is:
  1. For each category insert a little markup with category name, start/stop buttons and a little something to display the time passed
  2. For each category, generate a javascript to handle the start/stop timer and an ajax call to insert the time spent into the database.

That’s allSmile


Caveats: There are many things missing here: a repository, IoC, it only works for one user, a controller and views for categories and a bit many more, but all I wanted was a simple app and this is about as simple as you can get.

sâmbătă, 12 februarie 2011

Parsing T-SQL Queries

I’ve wondered at one point what happens when I click the little “Parse” button inside SSMS. The result is that the query is checked for syntax errors, but what really happens?

ParseSQL

It is amazingly simple: The query executed in SQL Server is:




SET PARSEONLY ON
select dbo.Category.CategoryId from dbo.Category
SET PARSONLY OFF

Yup. that’s all there is to it.