Showing posts with label NAnt. Show all posts
Showing posts with label NAnt. Show all posts

Friday, November 14, 2008

Unit testing databases with TSQLUnit

In preparation to doing a lot of database work (think schema/DDL changes) for the next several months, I thought it would be important to define methodologies and tools to manage the development process. Having been heavily applying a test-driven development (TDD) approach in a recent project for an ASP.NET C# web application, it seemed logical and now quite natural to continue pursuing a similar path and mindset but one now more targeted towards database development.

TSQLUnit and TDD

To facilitate a smooth flow with TDD, it was key to identify a suitable testing framework to support my goal. After some research and comparisons, I eventually settled on TSQLUnit, an xUnit framework for MS SQL Server. It is interesting to note that for every programming language that exists out there, it is expected that someone will eventually create an xUnit style testing framework for it. Why should SQL Server be any different?

Database testing could be performed from the application side but the nature of the development was primarily schema/DDL changes (e.g. mostly dropping and removing tables, columns, etc.) that involved very little or no business logic. Otherwise, app code would be a better and more appropriate candidate to handle testing the db. This particular scenario lent itself to finding and using something that was as close to the database as possible ideally where the unit tests can be written and supported by the TSQL language itself. The intent was to try and follow one of the core tenets of any xUnit framework which is to do testing in the same language as the code that requires to be changed. TSQLUnit seemed the best fit.

Regardless, it is quite probable, in the future, that I might disregard all of above and still switch to testing all aspects of the database using C#. This is not because of TSQLUnit but because the TSQL language itself can be so cumbersome to work with especially if you are accustomed to the power and flexibility of a non-declarative programming language such C#. For the moment, it is satisfies my current needs for testing so it will suffice.

A Few Key Features of TSQLUnit

One feature of TSQLUnit that is critical for any type of database testing is the ability to rollback changes after each test run completes ensuring that the database is in a nice clean state before the next test starts. This simply implies that the the data will not remain altered when the next test runs potentially tainting its results. Not only does this apply to all data/DML changes (inserts, updates, deletes) but DDL changes (creates, alters, drops) as well. For example, if Test A creates a table or alters a column for an existing table then those schema changes will most certainly rollback and be undone before Test B starts to execute. Therefore, the tests themselves are isolated from each other meaning that all of the unit tests can run independently.

In addition, TSQLUnit supports the xUnit features of Setup/Teardown for a test suite. For example, if you have five tests that are logically related and are dependent on the same preconditions then you can prepare all of your data in a single, shared 'Setup' fixture and TSQLUnit will automatically run this before each associated test thereby enforcing no co-dependencies between the tests. Note that one obvious drawback with repeating the same setup multiple times might be performance if the tests rely on loading large volumes of data for the purpose of testing. As for 'Teardown', it provides essentially the same functionality as 'Setup' with the one difference of executing at the end of each test and not at the beginning.

TSQLUnit is fairly decent but I initially struggled not with the framework itself but with the usual problems with database testing such as setup of data, rollback of changes, etc. It is a lot of work because unlike in application code it is hard to stub out dependencies in the database objects such as tables, stored procedires, etc. (Especially when dealing with a 225+ column table in the good ol' legacy db I was so fortunate to be tasked to make modifications.)

Running TSQLUnit using NAnt

Once a few initial unit tests were created using TSQLUnit, the process of running them was incorporated into the project's NAnt database build script. Just like the database build itself, the unit tests makes use of the NAntContrib <sql> task for their creation and execution. These are the high level targets defined in the build script:
<!-- after building database including applying change scripts -->

<call target="InstallTSQLUnit"/>
<call target="LoadUnitTests"/>
<call target="RunUnitTests"/>

'InstallTSQLUnit' is a single sql script that runs against your target db creating all of the necessary tables, stored procedures, etc. that the TSQLUnit framework requires to function. This is simply the basis and source code for the framework itself that is integrated with the database.

'LoadUnitTests' runs the sql scripts that contains the tests themselves located in a 'Tests' folder in the project's directory. To elaborate, it creates all of the unit test sprocs along with, if necessary, injecting any test data into the database that is not handled by the Setup fixtures.

Finally, 'RunUnitTests' calls the TSQLUnit command to run all of the unit tests inside the database:
<target name="RunUnitTests" description="Run all unit tests "
if="${installUnitTesting}">
<sql connstring="${connectionString}" transaction="true" delimiter=";" delimstyle="Line">
exec tsu_runTests;
</sql>
</target>
The NAnt db build is configurable so that you can specify whether or not to include or exclude the unit tests. This might be necessary if you just simply want to only build the database and nothing more perhaps for the sake of reducing the amount of the build time if the intent is to use database for another purpose other than testing.

In the command-line window, as NAnt logs the progress of the database build, at the end it will bubble up the output results of TSQLUnit indicating whether the tests passed or failed:
...
ExecuteSql:

[echo] loading .\Database\Tests\ut_DeleteTables_VerifyTablesDropped.sql

RunUnitTests:

[sql] ====================================================================
============
[sql] --------------------------------------------------------------------
------------
[sql]      SUCCESS!
[sql]
[sql]      14 tests, of which 0 failed and 0 had an error.
[sql]  Summary:
[sql]  Run tests ends:Feb 20 2008 11:44AM
[sql] --------------------------------------------------------------------
------------
[sql] Testsuite:  (14 tests ) execution time: 76 ms.
[sql] ====================================================================
============
[sql]  Run tests starts:Feb 20 2008 11:44AM
[sql] ====================================================================
============

BUILD SUCCEEDED

Total time: 6.1 seconds.
Currently, the overall database build does not fail if the unit tests themselves fail since NAnt can not catch and interpret TSQLUnit results. This codeproject MS SQL Server script validation with NAnt Task seems to provide that level integration between NAnt and SQL Server unit testing. Perhaps, this functionality might be implemented later, but, for now, it is good enough.

Other related posts on TSQLUnit:

It's True (And Not False): Assert Equality in TSQLUnit

Database schema changes unit tested using TSQLUnit

Sunday, July 27, 2008

Snake bitten by Python (R.I.P. NAnt)

After first experimenting last year with IronPython, the .NET port of Python, I decided to take the full plunge into Python itself leading to another major milestone in my programming career. Why? Because I now have an incredibly handy language, Python, that can superbly manage rudimentary but necessary development tasks. Furthermore, Python has enlightened me to yet another way of thinking about how code can be written.

As a dynamic language, Python can be extremely powerful. It can be used for "glue" tasks like scripting but its potential is even greater. By being an interpreted language, pieces of your code can both be written and tested practically at the same time via its interpreter console (as if the Immediate Window in Visual Studio were the means to simultaneously see how your code works while you are writing it. No "compilation tax".) In addition, Python can do both OOP (e.g. classes, et al) as well as functional programming (e.g. treating functions like data in lists and supporting lambdas like Lisp). Finally, with its leaner and less verbose syntax, less code is written as compared with other static languages.

After a few days to ramp up and get acquainted with the language, I immediately started to implement Python on a few things. NAnt build scripts and Windows bat files were the main targets for Python conversion. I also intend on rewriting in Python a C# .NET console tool that merges the content of multiple files into a single one. It seemed more natural and sensible to use Python for these types of development tasks.

Replacing NAnt with Python is favored since NAnt is an XML-based DSL that might be doing a little too much. The problem is not that it is a DSL, generally a good thing particularly when done right (as the Ant/NAnt folks succeeded quite well in doing to their credit), but the part of it being "XML-based". Who really wants to program all day in XML? Apache Ant, NAnt in the Java world, was a victim of the exploding popularity of XML during the height of the dot com era web applications. XML should be left to do what it does best and what it was originally intended for: basic structured data storage and configuration. Ant (and, subsequently, NAnt) should not have mixed the following two: (1) formatting/organizing data and (2) build flow logic. Not a far cry from violating the principle "separation of concerns".

If I can avoid it, I am finished with NAnt (or any other equivalent build frameworks that rely heavily on XML for its flow logic). If given the choice in a development environment, I probably not opt to use NAnt to handle build scripts. Not that I have anything against NAnt itself, just that better, more programmer friendly alternatives exist. NAnt was (and still is) a great option as compared, say, with the inferior Windows bat files or with the dev shops that manually build their projects via the Visual Studio. Instead, a dynamic language like Python (or BOO or Ruby or whatever else) is preferable to manage this type of work. (A few build automation frameworks do exist written in Python, but I will like to take a look at the promising, .NET born BOO build system.)

NAnt documentation mentions that it has the advantage over native OS shell commands because it is "cross-platform". That might be true, but Python has that area easily covered specifically with its 'os' and 'shutil' modules. Portability is one of Python's key features.

Code generation of other programming languages is another area where I also started to use Python. Database change scripts written in TSQL that are repetitive and voluminous have benefited significantly from using Python (as one example, creating structurally similar 'drop column' statements for 100+ columns). In the future, for other kinds of code generation (e.g. NHibernate mapping files is one example), I will definitely consider Python as a substitute for heavier code-gen tools such as MyGeneration.

The more I use Python, the more I am convinced that it will be employed as my general, all-purpose utility programming language. I intend to use it as a vital supporting player handling the grunt work in my development processes. It does not matter what the primary language happens to be whether C#, TSQL, etc. By and large, I just like how quick and dirty a script can be whipped up to perform some auxiliary task without having to endure the overhead of compiling, creating, and running some executable file. (Who knows? Maybe one day I can work on a major project where Python is the star of the show.) All in all, it is just such a nice clean, readable language making it far more enjoyable to work with as compared with something like NAnt.

To give an idea on how visually different it is to use Python over NAnt, below are the code of two identical build scripts written in each language. This the first script I ported over to Python. The script runs the SQL Server Database Publishing Wizard to generate a file that contains the sql to create the schema of a baseline database required at the start of each development cycle. The following high level tasks are executed by the script:
  • Create Schema Script- Generates the raw initial tsql schema script from target database using the DB pub wiz
  • Convert Script File Encoding- Convert file from Unicode to ASCII
  • Replace Script Values- Read from external csv file containing pairs of strings to replace in script.
  • Checkout File From Source Control - Checkout from Perforce the existing schema file that will be replaced.
  • Copy File To Build Location- Move sql script file to build directory
  • Build Database And Run Unit Tests- Run another separate script (currently written in NAnt) that builds the db and runs the unit tests using TSQLUnit
NAnt Version
<?xml version="1.0"?>
<project name="Generic Database Build" default="BaselineDatabaseCreation"
xmlns="http://nant.sf.net/release/0.85/nant.xsd">
<property name="base.dir" value=".\" overwrite="false" readonly ="false" />
<property name="sourceDB" value="" overwrite="false"/>
<property name="sourceServer" value=".\sqlDev2005" overwrite="false"/>
<property name="dbmsVersion" value="2000" overwrite="false"/>
<property name="connectionString" value="Server=${sourceServer};Database=${sourceDB};Trusted_Connection=True;"/>
<property name="dbBuild.dir" value="" overwrite="false"/>
<property name="targetDB" value="" overwrite="false"/>
<property name="targetServer" value=".\sqlDev2005" overwrite="false"/>
<property name="sqlScriptingTool.dir" value="C:\Program Files\Microsoft SQL Server\90\Tools\Publishing\" overwrite="false"/>
<property name="sqlScript.fileName" value="CreateSchema.sql" overwrite="false"/>
<property name="sqlScript.filePath" value="${path::combine(base.dir, sqlScript.fileName)}" overwrite="false"/>
<property name="sourceControl.filePath" value="${dbBuild.dir}Schema\${sqlScript.fileName}" overwrite="false"/>
<!-- replace values list variable -->
<property name="temp.fileName" value="temp.txt"/>
<property name="temp.filePath" value="${path::combine(base.dir, temp.fileName)}"/>
<property name="replaceValuesList.fileName" value="" overwrite="false"/>
<property name="replaceValuesList.filePath" value="${path::combine(base.dir, replaceValuesList.fileName)}"/>

<target name="BaselineDatabaseCreation" description="Creates baseline database tsql script end-to-end.">
<call target="CreateSchemaScript"/>
<call target="ConvertScriptFileEncoding"/>
<call target="ReplaceScriptValues" unless="${replaceValuesList.fileName==''}"/>
<call target="CheckoutFileFromSourceControl"/>
<call target="CopyNewScriptFileToBuildLocation"/>
<call target="GetSeedTablesData"/>
<call target="BuildDatabaseAndRunUnitTests" unless="${targetDB==''}"/>
</target>
<target name="CreateSchemaScript" description="Generates the raw initial tsql schema script from target database">
<delete file="${sqlScript.filePath}" if="${file::exists(sqlScript.filePath)}" />
<exec program="${sqlScriptingTool.dir}sqlpubwiz">
<arg value="script" />
<arg line="-C ${connectionString}" />
<arg value="${sqlScript.filePath}" />
<arg value="-schemaonly" />
<arg line="-targetserver ${dbmsVersion}" />
<!-- '-f' means overwrite existing files is true -->
<arg value="-f" />
</exec>
<fail message="${sqlScript.filePath} was not created."
unless="${file::exists(sqlScript.filePath)}" />
</target>
<target name="ConvertScriptFileEncoding" description="Convert file from Unicode to ASCII">
<copy file="${sqlScript.filePath}" tofile="${temp.filePath}" outputencoding="ASCII" overwrite="true" />
<move file="${temp.filePath}" tofile="${sqlScript.filePath}" overwrite="true"
unless="${file::exists(replaceValuesList.filePath)}" />
</target>
<target name="ReplaceScriptValues"
description="Read from external csv file containing pairs of strings to replace.">
<fail message="${replaceValuesList.filePath} does not exist."
unless="${file::exists(replaceValuesList.filePath)}" />
<foreach item="Line" in="${replaceValuesList.filePath}" delim="," property="x,y">
<echo message="Replacing '${x}' with '${y}'..." />
<copy file="${temp.filePath}" tofile="${sqlScript.filePath}" overwrite="true">
<filterchain>
<replacestring from="${x}" to="${y}" />
</filterchain>
</copy>
<copy file="${sqlScript.filePath}" tofile="${temp.filePath}" overwrite="true"/>
</foreach>
<move file="${temp.filePath}" tofile="${sqlScript.filePath}" overwrite="true"/>
</target>
<target name="CheckoutFileFromSourceControl" description="Checkout from source control the schema file that will be replaced.">
<fail message="${sourceControl.filePath} does not exist."
unless="${file::exists(sourceControl.filePath)}" />
<p4edit view="${sourceControl.filePath}">
<arg line="-t"/>
<arg line="text+k"/>
</p4edit>
</target>
<target name="CopyNewScriptFileToBuildLocation">
<copy file="${sqlScript.filePath}" tofile="${sourceControl.filePath}" overwrite="true" />
</target>
<target name="GetSeedTablesData">
<!--TODO: Create a separate NAnt build script for this-->
</target>
<target name="BuildDatabaseAndRunUnitTests">
<nant buildfile="GenericDatabase.build" inheritall="false" >
<properties>
<property name="base.dir" value="${dbBuild.dir}"/>
<property name="server" value="${targetServer}" />
<property name="database" value="${targetDB}"/>
<property name="includeUnitTesting" value="true" />
</properties>
</nant>
</target>
</project>

Python Version
import os
import csv
import shutil

sqlscripting_tool=r'C:\Program Files\Microsoft SQL Server\90\Tools\Publishing\sqlpubwiz.exe'
dbms_version = '2000'
connection_string = 'Server=' + source_server + ';Database=' + source_db + ';Trusted_Connection=True;'

sqlscript_filename = 'CreateSchema.sql'
sqlscript_filepath = os.path.join(sqlscript_dir, sqlscript_filename)
source_control_filepath = os.path.join(db_build_dir, sqlscript_filename)

replace_values_filepath = os.path.join(replace_values_dir, 'ReplaceList.csv')
error_found_message = 'Error found!'

def run_script():
"""Run all tasks"""

tasks = [create_schema_script, convert_scriptfile_encoding, replace_script_values,
checkout_file_from_source_control, copy_file_to_build_location, build_database_and_run_unit_tests]
for task in tasks:
print 'Executing \'' + task.func_name + '\'... '
is_successful = task()
if not is_successful:
print 'Script Failure!'
break

if is_successful:
print 'Script Success!'

def create_schema_script():
"""Generates the raw initial tsql schema script from target database"""

if os.path.isfile(sqlscript_filepath):
os.remove(sqlscript_filepath)

args = ['sqlpubwiz', 'script', '-C ' + connection_string, '"' + sqlscript_filepath + '"',
'-schemaonly', '-targetserver ' + dbms_version, '-f']
os.spawnv(os.P_WAIT, sqlscripting_tool, args)

if os.path.isfile(sqlscript_filepath) == False:
print error_found_message
print "File '" + sqlscript_filepath + "' was not created."
return False

return True

def convert_scriptfile_encoding():
"""Convert file from Unicode to ASCII"""

cmd1 = 'type "' + sqlscript_filepath + '" > temp.txt'
cmd2 = 'move temp.txt "' + sqlscript_filepath + '"'
cmds = [cmd1, cmd2]
for cmd in cmds:
dos = os.popen(cmd)
dos.read()
dos.close()

return True

def replace_script_values():
""" Read from external csv file containing pairs of strings to replace values in sql script. """

# if 'replace values' list not provided then assume not needed
if replace_values_filepath == '':
return False

# check for 'replace values' file existence
if os.path.isfile(replace_values_filepath) == False:
print error_found_message
print "Replace values list file '" + replace_values_filepath + "' does not exist."
return False

# modify file content with new values
f = open(sqlscript_filepath, 'r')
text = f.read()
replace_values = csv.reader(open(replace_values_filepath, 'r'))
for row in replace_values:
find_text = row[0]
replace_with_text = row[1]
text = text.replace(find_text, replace_with_text)
f.close()

# write to script file with new values
f = open(sqlscript_filepath, 'w')
f.write(text)
f.close()

return True

def checkout_file_from_source_control():
""" Checkout from source control the schema file that will be replaced. """

# look for source control file
if os.path.isfile(source_control_filepath) == False:
print error_found_message
print "Source control file '" + source_control_filepath + "' does not exist."
return False

# checkout file (note: could use PyPerforce API framework instead)
cmd = 'p4 edit -t text+k ' + source_control_filepath + ''
p4 = os.popen(cmd)
p4.read()
p4.close()

return True

def copy_file_to_build_location():
""" Move sql script file to build directory """

shutil.copy(sqlscript_filename, source_control_filepath)

return True

def build_database_and_run_unit_tests():
""" Build database and validate schema by running unit tests """

nant_tool = os.path.join(base_dir, 'Tools\\NAnt\\bin\\', 'NAnt.exe')
build_script_filepath = os.path.join(base_dir, 'Projects\\Libs\\Utils\\NAntScripts\\DatabaseBuilds\\', 'GenericDatabase.build')
build_dir = os.path.split(os.path.normpath(db_build_dir))[0] # hack: need to remove 'Schema' folder; todo: need to remove this from generic db build script

# todo: replace NAnt script with Python script
args = ['NAnt', '-buildfile:' + build_script_filepath, '-D:base.dir=' + build_dir,'-D:server=' + target_server,
'-D:database=' + target_db, '-D:installUnitTesting=' + 'true', ]
os.spawnv(os.P_WAIT, nant_tool, args)

return True

run_script()