Click here to Skip to main content
15,895,839 members
Articles / Database Development / SQL Server

Armadillo: Unit Testing of inline SQL with a little help from Attributes and Reflection

Rate me:
Please Sign up or sign in to vote.
4.65/5 (23 votes)
28 Nov 2005CPOL13 min read 67.3K   930   58  
This article provides a way for automatically testing SQL embedded in your Data Access Layer (DAL) before publishing a new release of your application.
// (c) Pedro J. Molina, 2005, Spain. http://pjmolina.com
// Feel free to use it and improve it.
using System;
using System.Data;
using System.Collections;
using System.Reflection;
using OKOKO.Armadillo.Attributes;
using NUnit.Framework;

namespace OKOKO.Armadillo.Framework
{
	/// <summary>
	/// Armadillo TestEngine
	/// </summary>
	[TestFixture()]
	public class TestEngine
	{
		#region Private Members
		private Assembly Asm;
		private IDbInfo DbInfo;
		private IDbConnection Cnx;

		private int countClass, countMethods, countOK, countFail;
		private int countClassMethods, classOK, classFail;
		#endregion

		#region Constructor
		/// <summary>
		/// Default Constructor
		/// </summary>
		/// <param name="asm">Assembly</param>
		/// <param name="dbInfo">DB connection info.</param>
		public TestEngine(Assembly asm, IDbInfo dbInfo)
		{
			DbInfo = dbInfo;	//DB information for connection & execution
			Cnx = dbInfo.Cnx;	//Connection
			Asm = asm;			//Assembly to be tested
			TestSql();
		}
		#endregion

		#region Test Driver
		/// <summary>
		/// TestDriver. Drives all the tests performed by Armadillo using the power of Reflection.
		/// </summary>
		[Test()]
		public void TestSql()
		{
			countClass = countMethods = countOK = countFail = 0;

			//Check code using reflection
			foreach(Type exType in Asm.GetExportedTypes())
			{
				//Check each class of the assembly
				if ((exType.IsClass) && (IsADalClass(exType)))
				{
					countClass++;
					countClassMethods = classOK = classFail = 0;
					Console.WriteLine("Testing Class: [{0}]", exType.FullName);

					foreach(MethodInfo mi in exType.GetMethods(BindingFlags.Public|BindingFlags.Static))
					{
						//Check each method
						if (IsLabelled(mi, typeof(SqlTextQueryAttribute)))
						{
							countClassMethods++;
							Console.Write("  Testing SqlTextQuery   Method: ");
							CheckSqlTextQuery(mi);
						}
						if (IsLabelled(mi, typeof(SqlTextExecuteAttribute)))
						{
							countClassMethods++;
							Console.Write("  Testing SqlTextExecute Method: ");
							CheckSqlTextExecute(mi);
						}
						if (IsLabelled(mi, typeof(SqlQueryAttribute)))
						{
							countClassMethods++;
							Console.Write("  Testing SqlQuery       Method: ");
							CheckSqlQuery(mi);
						}
						if (IsLabelled(mi, typeof(SqlExecuteAttribute)))
						{
							countClassMethods++;
							Console.Write("  Testing SqlExecute     Method: ");
							CheckSqlExecute(mi);
						}
					}
					Console.WriteLine("Class [{0}] tested. Results: OK: {1}/{2} Failed:{3}", exType.FullName,
						classOK, countClassMethods, classFail);

					countMethods += countClassMethods;
					countFail += classFail;
					countOK += classOK;
				}
			}
			Console.WriteLine("-----------------------------------------------------------------");
			Console.WriteLine("Final results: {0} classes & {1} methods tested.", countClass,countMethods);
			Console.WriteLine("  Results: OK: {0}/{1} Failed: {2}", countOK, countMethods, countFail);

			if ( countFail>0 )	
				Assert.Fail("{0} methods failed.", countFail); //Fail if needed

		}
		#endregion

		#region Test Methods
		/// <summary>
		/// Gest an sql query string from the method and let Sql server execute it.
		/// </summary>
		/// <param name="mi">Method to check</param>
		public void CheckSqlTextQuery(MethodInfo mi)
		{
			try 
			{
				object[] pars = CreateDefaultParams(mi);
				string sql = mi.Invoke(null, pars) as string;
	
				IDbCommand cmd = DbInfo.CommandFactory(sql);		//Create the command
				int res = cmd.ExecuteNonQuery();				//Launch the SQL 
				LogOK(mi);										//Log OK
			}
			catch (Exception ex)
			{
				LogException(mi, ex);							//Log fail
			}
		}

		/// <summary>
		/// Gest an sql string from the method and let Sql server parse it.
		/// </summary>
		/// <param name="mi">Method to check</param>
		public void CheckSqlTextExecute(MethodInfo mi)
		{
			try 
			{
				DbInfo.DisableSqlExecution();					//Disable SQL Execution
				object[] pars = CreateDefaultParams(mi);
				string sql = mi.Invoke(null, pars) as string;

				IDbCommand cmd = DbInfo.CommandFactory(sql);	//Create the command
				int res = cmd.ExecuteNonQuery();				//Launch the SQL 
				LogOK(mi);										//Log OK
			}
			catch (Exception ex)
			{
				LogException(mi, ex);							//Log fail
			}
			finally
			{
				DbInfo.EnableSqlExecution();					//Enable SQL Execution
			}
		}

		/// <summary>
		/// Launch a query method
		/// </summary>
		/// <param name="mi">Method to be checked</param>
		public void CheckSqlQuery(MethodInfo mi)
		{
			try 
			{
				object[] pars = CreateDefaultParams(mi);
				mi.Invoke(null, pars);							//Execute method
				LogOK(mi);
			}
			catch (Exception ex)
			{
				LogException(mi, ex);
			}
		}
		/// <summary>
		/// Executes an method that launches an SQL statement
		/// </summary>
		/// <param name="mi">Method to be tested</param>
		public void CheckSqlExecute(MethodInfo mi)
		{
			try 
			{
				DbInfo.DisableSqlExecution();					//Disable SQL Execution

				object[] pars = CreateDefaultParams(mi);
				mi.Invoke(null, pars);							//Execute method
				LogOK(mi);
			}
			catch (Exception ex)
			{
				LogException(mi, ex);
			}
			finally
			{
				DbInfo.EnableSqlExecution();					//Enable SQL Execution
			}
		}
		#endregion

		#region Auxiliary Reflection Methods
		/// <summary>
		/// Checks if the current class is part of the DAL or not
		/// (has to be tested or not)
		/// </summary>
		/// <param name="cl">Current class</param>
		/// <returns>True if it is marked as a DalLClass</returns>
		private bool IsADalClass(Type cl)
		{
			object[] attrs = cl.GetCustomAttributes(typeof(DalClassAttribute), true);
			return (attrs.Length>0);
		}

		/// <summary>
		/// Checks if a method is labelled with an specific attribute.
		/// </summary>
		/// <param name="mi">Method to inspect</param>
		/// <param name="attributeType">Attribute to look for</param>
		/// <returns>True if its labelled</returns>
		private bool IsLabelled(MethodInfo mi, Type attributeType)
		{
			object[] attrs = mi.GetCustomAttributes(attributeType, true);
			return (attrs.Length>0);
		}

		/// <summary>
		/// Extracts ParamValue attributes for a method.
		/// </summary>
		/// <param name="mi">Method</param>
		/// <returns>A hashtable of paramvalues indexed by param name</returns>
		private Hashtable RetrieveParamsValues(MethodInfo mi)
		{
			Hashtable result = new Hashtable();
			foreach(ParamValueAttribute parval in mi.GetCustomAttributes(typeof(ParamValueAttribute), true))
			{
				result.Add(parval.ParamName, parval);    
			}
			return result;
		}


		/// <summary>
		/// Creates a default value for each parameter
		/// </summary>
		/// <param name="mi">Method to be invoked</param>
		/// <returns>An array with the values created</returns>
		private object[] CreateDefaultParams(MethodInfo mi)
		{
			Hashtable paramsValues = RetrieveParamsValues(mi);

			ArrayList parameters = new ArrayList();
			foreach(ParameterInfo par in mi.GetParameters())
			{
				//1. Checks for param-value provided 
				if (paramsValues.ContainsKey(par.Name))
				{
					//use it
					parameters.Add(((ParamValueAttribute)paramsValues[par.Name]).Value);
				}
				else
				{
					//2. if not provided, 
					//Let's invent values for common simple types
					switch (par.ParameterType.FullName)
					{
						case "System.String": 
							parameters.Add("Hi!");
							break;
						case "System.Int16": 
							parameters.Add(Int16.Parse("0"));
							break;
						case "System.Int32": 
							parameters.Add(Int32.Parse("0"));
							break;
						case "System.Int64": 
							parameters.Add(Int64.Parse("0"));
							break;
						case "System.Decimal": 
							parameters.Add(Decimal.Parse("0"));
							break;
						case "System.Float": 
							parameters.Add(System.Single.Parse("0"));
							break;
						case "System.Double": 
							parameters.Add(Double.Parse("0"));
							break;
						case "System.Boolean": 
							parameters.Add(false);
							break;
						case "System.DateTime": 
							parameters.Add(new DateTime(2000,1,2,3,4,5,6));
							break;
						default:
							parameters.Add(null);
							break;
					}
				}
			}
			return parameters.ToArray();
		}


		#endregion

		#region Log exceptions / OKs
		private void LogException(MethodInfo mi, Exception ex)
		{
			classFail++;
			Console.WriteLine(" -FAIL {0}()", mi.Name);
			if ((ex.InnerException != null) && (ex.InnerException.GetType()==DbInfo.TypeException))
			{
				Console.WriteLine("    >> {0}", ex.InnerException.Message);	//The especific Exception we where looking for
			}
			else
			{
				if (ex.GetType()==DbInfo.TypeException)
				{
					Console.WriteLine("    >> {0}", ex.Message);	//The especific Exception we where looking for
				}
				else
				{
					Console.WriteLine("    >> {0}", ex.ToString()); //Other Exception
				}
			}
		}
		private void LogOK(MethodInfo mi)
		{
			classOK++;
			Console.WriteLine(" +OK   {0}()", mi.Name);
		}
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Icinetic
Spain Spain
Ph.D in Computer Sciences. My research interest is focus on Conceptual Models, Software Engineering, Code Generation, Model Execution, & UI Patterns.
I work for Icinetic, Seville, Spain building http://Radarc.net and other MDD technologies.

Comments and Discussions