SSDL:简单的自测试数据层





0/5 (0投票)
一种在.NET中调用和管理存储过程的简便方法。
引言
和大多数人一样,我偶尔会编写一些工具来整合应用程序中数据库访问和对象填充的逻辑。对于仅仅调用存储过程,我认为LINQ to SQL和Entity Framework过于冗长和状态化,无论是在生成的代码还是基本用法方面。在更有限的范围内,可以创建一些更容易使用且实现更简洁的东西。
背景
这个工具叫做SSDL,即简单自测数据层,因为这次我决定将一起改变的文件放在一起。
虽然我认为SSDL的整体设计很好,但其中这个非技术部分,在我看来是最重要的。它也可能引起一些争议;有些人认为测试应该放在一个单独的项目中。我不同意。如果你对集成测试有任何疑虑,可以看看这篇文章,它是一篇很好的读物。
文件嵌套不是使用SSDL其他部分的必需,但强烈推荐。
使用代码
每个开发机器上只需一次:将此合并到您的注册表中,然后重启。您必须重启才能使更改生效。
;this causes CS projects to nest .sql.cs and .test.cs files under .sql files of the same name
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0_Config\
Projects\{FAE04EC0-301F-11d3-BF4B-00C04F79EFBC}\RelatedFiles\.sql]
".cs"=dword:00000002
".test.cs"=dword:00000001
;this causes VB projects to nest .sql.vb and .test.vb files under .sql files of the same name
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0_Config\
Projects\{F184B08F-C81C-45f6-A57F-5ABD9991F28F}\RelatedFiles\.vb]
".vb"=dword:00000002
".test.vb"=dword:00000001
现在您可以创建第一个SSDL项目了。
步骤 1:创建一个新的测试项目,并添加对Pivot.dll和Pivot.Data.dll的引用。
步骤 2:添加一个类来支持对数据库的存储过程调用。例如
namespace Customers.Data
{
internal static class CustomerDatabase
{
public static TDelegate StoredProcedure<tdelegate>() where TDelegate : class
{
return Pivot.Data.StoredProcedureCaller<tdelegate>.GetMethod(() =>
"(your connection string)");
}
}
}
步骤 3:为您的第一个存储过程添加三个文件
- (存储过程名).sql
- (存储过程名).sql.cs
- (存储过程名).test.cs
这些文件应该会自动嵌套。如果不行,请确保在合并SSDL注册表更改后重启。
示例项目中的一个文件三元组
IF EXISTS (SELECT * FROM sys.objects WHERE
object_id = OBJECT_ID(N'[dbo].[FindCustomers]') AND type in (N'P', N'PC'))
DROP PROCEDURE [dbo].[FindCustomers]
GO
CREATE PROCEDURE [dbo].[FindCustomers]
@NameContains varchar(50)
AS
SELECT
[CustomerID]
,[CustomerName]
,[EmailAddress]
FROM [Customer]
WHERE [CustomerName] LIKE '%' + @NameContains + '%'
namespace Customers.Data.StoredProcedures
{
public static partial class FindCustomers
{
public static definition Execute =
CustomerDatabase.StoredProcedure<definition>();
public delegate Result[] definition(string NameContains);
public class Result
{
public int CustomerID;
public string CustomerName;
public string EmailAddress;
}
}
}
在这段代码中,FindCustomers.Execute
是调用存储过程[FindCustomers]
的方法。它会做您期望它做的事情;它会创建一个Result
的数组,并用检索到的数据填充其字段。
更一般地说...
存储过程的名称取自委托的名称,或者像本例中,如果委托的名称是“definition”,则取自其声明类的名称。
委托定义中的参数应与存储过程接受的参数相匹配。
您可以将多个返回列表示为委托返回类型中的字段(而不是属性),如这里的Result
类所示。此处包含的字段假定为必需的。如果您的存储过程只返回一个值或列,您可以使用原始类型(如string
或int
),或原始类型的数组。
注意:是的,自动生成此文件中的代码会相当容易。我们还可以构建一个工具来检查所有.sql文件是否与目标数据库的内容匹配,或者从现有存储过程集合中自动生成许多SQL-代码-测试文件三元组。我可能会在下一个版本中通过一个插件支持这些以及其他功能。
最后,测试
using System;
using System.Linq;
using Customers.Data.TestHelpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Customers.Data.StoredProcedures
{
partial class FindCustomers
{
[TestClass]
public class Tests : RollbackTest
{
[TestMethod]
public void FindCustomers()
{
var uniqueTag = Guid.NewGuid().ToString();
var customersBeforeAdds = Execute(uniqueTag);
var addCustomerName1 = "test customer 1 " + uniqueTag + "###";
CreateNewCustomer.Execute(addCustomerName1, "");
var customersAfterAdds = Execute(uniqueTag);
Assert.AreEqual(customersBeforeAdds.Count() + 1,
customersAfterAdds.Count());
Assert.AreEqual(1, customersAfterAdds.
Where(o => o.CustomerName == addCustomerName1).Count());
}
}
}
}
完成前三个文件后,您可以复制粘贴.sql文件,将其用作添加更多文件的模板。其他两个文件会随之而来,并根据这个顶级文件进行重命名。打开这三个文件,进行搜索和替换,一次性更新类、测试和存储过程的名称。
关注点
SSDL的存储过程调用是通过一个非常有用的函数实现的,我称之为通用方法枢轴(pivoter)。
/// <summary>
/// Returns a method of type TDelegate that calls
/// a pivot function you supply. All functions must be static.
/// </summary>
/// <typeparam name="TDelegate">The delegate you wish to create a method
/// for using the supplied pivot function. Must have a return
/// type of void, IConvertable, or new().</typeparam>
/// <typeparam name="TContext">the type of execution context
/// used by the supplied pivoters</typeparam>
/// <param name="PivotMethodRetriever">a static function that
/// will supply a method with the pivot signature
/// [return type of TDelegate] (object[] args, TContext context)</param>
/// <param name="ContextRetreiver">a static function
/// that the supplied pivoters will use to retrieve a context of execution</param>
/// <returns>a method of type TDelegate that calls the supplied pivot function</returns>
public static TDelegate Pivot<TDelegate, TContext>(
Func<MethodInfo> PivotMethodRetriever,
Func<TContext> ContextRetreiver
) where TDelegate : class
{
TDelegate ret = null;
string pivotMethodRetrieverName = "";
try
{
if (PivotMethodRetriever == null)
throw new ArgumentNullException("PivotMethodRetriever");
if (!PivotMethodRetriever.Method.IsStatic)
throw new ArgumentException("This function must be static.",
"PivotMethodRetriever");
if (ContextRetreiver == null)
throw new ArgumentNullException("ContextRetreiver");
if (!ContextRetreiver.Method.IsStatic)
throw new ArgumentException("This function must be static.",
"ContextRetreiver");
Type delegateType = typeof(TDelegate);
if (!typeof(MulticastDelegate).IsAssignableFrom(delegateType))
throw new ArgumentException(string.Format(
"type {0} is not a delegate type", delegateType.FullName));
MethodInfo pivotMethod = PivotMethodRetriever.Invoke();
CreatedMethods<TDelegate>.Pivots.TryGetValue(pivotMethod, out ret);
if (ret != null) //prevent duplication
return ret;
MethodInfo delegateInvokeMethod = delegateType.GetMethod("Invoke");
Type delegateReturnType = delegateInvokeMethod.ReturnType;
Type[] paramTypes =
delegateInvokeMethod.GetParameters().Select(
(ParameterInfo p) => p.ParameterType).ToArray();
pivotMethodRetrieverName = pivotMethod.DeclaringType.FullName +
"." + pivotMethod.Name;
var newMethodName = "dyn__" + pivotMethodRetrieverName +
"__" + delegateType.FullName;
// check to see if the pivot method has the required
// signature here, otherwise a scary and confusing
// SecurityVerificationException ("Operation could destabilize
// the runtime") could be thrown later when the constructed method is invoked
if (!pivotMethod.ReturnType.IsAssignableFrom(delegateReturnType))
ThrowInvalidSignatureError(pivotMethodRetrieverName);
var pivotMethodParams = pivotMethod.GetParameters();
if (pivotMethodParams.Count() != 2)
ThrowInvalidSignatureError(pivotMethodRetrieverName);
var firstPivotMethodParamType = pivotMethodParams.First().ParameterType;
if (firstPivotMethodParamType != typeof(object[]))
ThrowInvalidSignatureError(pivotMethodRetrieverName);
var secondPivotMethodParamType = pivotMethodParams.Skip(1).First().ParameterType;
if (secondPivotMethodParamType != typeof(TContext))
ThrowInvalidSignatureError(pivotMethodRetrieverName);
DynamicMethod dyn = new DynamicMethod(newMethodName,
delegateReturnType, paramTypes, true);
ILGenerator il = dyn.GetILGenerator();
//load all the arguments this method was called with into
//an object array and push it onto the stack
LocalBuilder locArgs = il.DeclareLocal(typeof(object[]));
il.Emit(OpCodes.Ldc_I4, dyn.GetParameters().Count());
il.Emit(OpCodes.Newarr, typeof(object));
for (int i = 0; i <= paramTypes.GetUpperBound(0); i++)
{
il.Emit(OpCodes.Stloc, locArgs.LocalIndex);
il.Emit(OpCodes.Ldloc, locArgs.LocalIndex);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldarg, i);
il.Emit(OpCodes.Box, paramTypes[i]);
il.Emit(OpCodes.Stelem, typeof(object));
il.Emit(OpCodes.Ldloc, locArgs.LocalIndex);
}
//call the context retriever method to load a context value onto the stack
il.Emit(OpCodes.Call, ContextRetreiver.Method);
//call the supplied method
il.Emit(OpCodes.Call, pivotMethod);
//mandatory return at end of method call
il.Emit(OpCodes.Ret);
ret = (TDelegate)(object)dyn.CreateDelegate(delegateType);
CreatedMethods<TDelegate>.Pivots.Add(pivotMethod, ret);
Debug.WriteLine(string.Format("created method {0}", dyn.Name), "Pivoter");
}
catch (InvalidProgramException ex)
//this should be impossible if the above code
// is correct, but we'll catch it just the same
{
throw new InvalidOperationException(string.Format(
"Method supplied for pivoter {0} for delegate {1} is invalid.",
pivotMethodRetrieverName, typeof(TDelegate).FullName, ex));
}
return ret;
}
调用此函数将返回一个类型为TDelegate
的方法。这个构造的方法反过来又调用由PivotMethodRetriever
参数提供的一个方法,该参数必须提供一个具有以下签名的函数
TReturnType PivotFunction<TDelegate, TReturnType>(object[] callingArgs, TContext context)
其中TReturnType
必须是TDelegate
的返回类型。
为了调用存储过程,SSDL(Pivot.Data.dll)根据TDelegate
的返回类型为Pivot提供五种不同的枢轴方法之一,取决于以下几种情况
void
(执行非查询)- 单个
IConvertible
类型 IConvertible
类型元素的数组- 单个可构造类型
- 可构造类型元素的数组
以下是最后一种情况的枢轴,当TDelegate
期望返回可构造类型元素的数组时。
private static TReturnType[] GetItems<TReturnType>(object[] callingArgs,
string ConnectionString) where TReturnType : new()
{
TReturnType[] ret = new TReturnType[0];
using (DataTable dt = ExecuteReturnTable(callingArgs, ConnectionString))
{
if ((dt != null) && dt.Columns.Count > 0)
{
int rowUpperBound = dt.Rows.Count - 1;
ret = new TReturnType[rowUpperBound + 1];
DataColumn[] cols = GetDataColumns(dt);
for (int rowIndex = 0; rowIndex <= rowUpperBound; rowIndex++)
{
var n = new TReturnType();
ret[rowIndex] = n;
for (int colIndex = 0; colIndex < ReturnTypeFieldCount; colIndex++)
{
PopulateField(n, dt.Rows[rowIndex],
ReturnTypeFields[colIndex], cols[colIndex]);
}
}
}
}
return ret;
}
方法枢轴(Method-pivoting)的用途不仅限于调用存储过程;它还可以帮助管理任何两个应用程序层之间的过渡,当您需要一种通用的方法来同时处理运行时I/O及其相关的静态元数据时。我将在另一个项目中提供另一个示例。
结论
如果您使用存储过程,SSDL可以轻松地将测试创建集成到您的开发过程中,从而使测试覆盖率更有可能始终完整。作为额外的好处,代码使用非常简单,所有SQL、代码和测试都被组织成整洁的、自包含的单元。
历史
- 2011年11月7日:首次发布。