有没有人在unit testingSQL存储过程中取得任何成功?

我们发现我们为C#/ C ++代码编写的unit testing已经真正的得到了回报。 但是我们在存储过程中仍然有数千行业务逻辑,只有当我们的产品推广到大量用户时才真正得到testing。

更糟的是,这些存储过程中的一些最终会很长,因为在SP之间传递临时表时性能会受到影响。 这阻止了我们重构使代码更简单。

我们在围绕一些关键存储过程(主要testing性能)build立unit testing方面做了几次尝试,但是发现为这些testing设置testing数据确实很困难。 例如,我们最终复制testing数据库。 除此之外,testing最终变得非常敏感,甚至是对存储过程的最小改变。 或表格需要对testing进行大量的更改。 因此,在由于这些数据库testing间歇性失败而导致许多构build中断之后,我们不得不将它们从构build过程中提取出来。

所以,我的问题的主要部分是:有没有人成功地为他们的存储过程写unit testing?

我的问题的第二部分是linqunit testing是否会更容易?

我在想,不必设置testing数据表,你可以简单地创build一个testing对象的集合,并在“linq to objects”的情况下testing你的linq代码? (我是一个完全陌生的linq,所以不知道这是否甚至可以工作)

我碰到了同样的问题,并发现如果我为数据访问创build了一个简单的抽象基类,允许我注入一个连接和事务,我可以unit testing我的sprocs,看看他们是否在SQL中做了这样的工作,要求他们做,然后回滚,所以没有testing数据留在数据库中。

这比通常的“运行一个脚本来设置我的testing数据库,然后在testing运行之后清理垃圾/testing数据”感觉更好。 这也更接近于unit testing,因为这些testing可以单独运行,而不需要在运行这些testing之前有很多“db中的所有东西都需要”。

这是用于数据访问的抽象基类的一个片段

Public MustInherit Class Repository(Of T As Class) Implements IRepository(Of T) Private mConnectionString As String = ConfigurationManager.ConnectionStrings("Northwind.ConnectionString").ConnectionString Private mConnection As IDbConnection Private mTransaction As IDbTransaction Public Sub New() mConnection = Nothing mTransaction = Nothing End Sub Public Sub New(ByVal connection As IDbConnection, ByVal transaction As IDbTransaction) mConnection = connection mTransaction = transaction End Sub Public MustOverride Function BuildEntity(ByVal cmd As SqlCommand) As List(Of T) Public Function ExecuteReader(ByVal Parameter As Parameter) As List(Of T) Implements IRepository(Of T).ExecuteReader Dim entityList As List(Of T) If Not mConnection Is Nothing Then Using cmd As SqlCommand = mConnection.CreateCommand() cmd.Transaction = mTransaction cmd.CommandType = Parameter.Type cmd.CommandText = Parameter.Text If Not Parameter.Items Is Nothing Then For Each param As SqlParameter In Parameter.Items cmd.Parameters.Add(param) Next End If entityList = BuildEntity(cmd) If Not entityList Is Nothing Then Return entityList End If End Using Else Using conn As SqlConnection = New SqlConnection(mConnectionString) Using cmd As SqlCommand = conn.CreateCommand() cmd.CommandType = Parameter.Type cmd.CommandText = Parameter.Text If Not Parameter.Items Is Nothing Then For Each param As SqlParameter In Parameter.Items cmd.Parameters.Add(param) Next End If conn.Open() entityList = BuildEntity(cmd) If Not entityList Is Nothing Then Return entityList End If End Using End Using End If Return Nothing End Function End Class 

接下来您将看到一个使用上述基础的示例数据访问类来获取产品列表

 Public Class ProductRepository Inherits Repository(Of Product) Implements IProductRepository Private mCache As IHttpCache 'This const is what you will use in your app Public Sub New(ByVal cache As IHttpCache) MyBase.New() mCache = cache End Sub 'This const is only used for testing so we can inject a connectin/transaction and have them roll'd back after the test Public Sub New(ByVal cache As IHttpCache, ByVal connection As IDbConnection, ByVal transaction As IDbTransaction) MyBase.New(connection, transaction) mCache = cache End Sub Public Function GetProducts() As System.Collections.Generic.List(Of Product) Implements IProductRepository.GetProducts Dim Parameter As New Parameter() Parameter.Type = CommandType.StoredProcedure Parameter.Text = "spGetProducts" Dim productList As List(Of Product) productList = MyBase.ExecuteReader(Parameter) Return productList End Function 'This function is used in each class that inherits from the base data access class so we can keep all the boring left-right mapping code in 1 place per object Public Overrides Function BuildEntity(ByVal cmd As System.Data.SqlClient.SqlCommand) As System.Collections.Generic.List(Of Product) Dim productList As New List(Of Product) Using reader As SqlDataReader = cmd.ExecuteReader() Dim product As Product While reader.Read() product = New Product() product.ID = reader("ProductID") product.SupplierID = reader("SupplierID") product.CategoryID = reader("CategoryID") product.ProductName = reader("ProductName") product.QuantityPerUnit = reader("QuantityPerUnit") product.UnitPrice = reader("UnitPrice") product.UnitsInStock = reader("UnitsInStock") product.UnitsOnOrder = reader("UnitsOnOrder") product.ReorderLevel = reader("ReorderLevel") productList.Add(product) End While If productList.Count > 0 Then Return productList End If End Using Return Nothing End Function End Class 

现在在你的unit testing中,你也可以从一个非常简单的基类inheritance你的设置/回滚工作 – 或者保持这个unit testing的基础

下面是我使用的简单testing基类

 Imports System.Configuration Imports System.Data Imports System.Data.SqlClient Imports Microsoft.VisualStudio.TestTools.UnitTesting Public MustInherit Class TransactionFixture Protected mConnection As IDbConnection Protected mTransaction As IDbTransaction Private mConnectionString As String = ConfigurationManager.ConnectionStrings("Northwind.ConnectionString").ConnectionString <TestInitialize()> _ Public Sub CreateConnectionAndBeginTran() mConnection = New SqlConnection(mConnectionString) mConnection.Open() mTransaction = mConnection.BeginTransaction() End Sub <TestCleanup()> _ Public Sub RollbackTranAndCloseConnection() mTransaction.Rollback() mTransaction.Dispose() mConnection.Close() mConnection.Dispose() End Sub End Class 

最后 – 下面是一个简单的testing,使用该testing基类,显示如何testing整个CRUD周期,以确保所有的sprocs做他们的工作,你的ado.net代码左右映射正确

我知道这不会testing上面的数据访问示例中使用的“spGetProducts”sproc,但您应该看到这种方法背后的力量sprocs

 Imports SampleApplication.Library Imports System.Collections.Generic Imports Microsoft.VisualStudio.TestTools.UnitTesting <TestClass()> _ Public Class ProductRepositoryUnitTest Inherits TransactionFixture Private mRepository As ProductRepository <TestMethod()> _ Public Sub Should-Insert-Update-And-Delete-Product() mRepository = New ProductRepository(New HttpCache(), mConnection, mTransaction) '** Create a test product to manipulate throughout **' Dim Product As New Product() Product.ProductName = "TestProduct" Product.SupplierID = 1 Product.CategoryID = 2 Product.QuantityPerUnit = "10 boxes of stuff" Product.UnitPrice = 14.95 Product.UnitsInStock = 22 Product.UnitsOnOrder = 19 Product.ReorderLevel = 12 '** Insert the new product object into SQL using your insert sproc **' mRepository.InsertProduct(Product) '** Select the product object that was just inserted and verify it does exist **' '** Using your GetProductById sproc **' Dim Product2 As Product = mRepository.GetProduct(Product.ID) Assert.AreEqual("TestProduct", Product2.ProductName) Assert.AreEqual(1, Product2.SupplierID) Assert.AreEqual(2, Product2.CategoryID) Assert.AreEqual("10 boxes of stuff", Product2.QuantityPerUnit) Assert.AreEqual(14.95, Product2.UnitPrice) Assert.AreEqual(22, Product2.UnitsInStock) Assert.AreEqual(19, Product2.UnitsOnOrder) Assert.AreEqual(12, Product2.ReorderLevel) '** Update the product object **' Product2.ProductName = "UpdatedTestProduct" Product2.SupplierID = 2 Product2.CategoryID = 1 Product2.QuantityPerUnit = "a box of stuff" Product2.UnitPrice = 16.95 Product2.UnitsInStock = 10 Product2.UnitsOnOrder = 20 Product2.ReorderLevel = 8 mRepository.UpdateProduct(Product2) '**using your update sproc '** Select the product object that was just updated to verify it completed **' Dim Product3 As Product = mRepository.GetProduct(Product2.ID) Assert.AreEqual("UpdatedTestProduct", Product2.ProductName) Assert.AreEqual(2, Product2.SupplierID) Assert.AreEqual(1, Product2.CategoryID) Assert.AreEqual("a box of stuff", Product2.QuantityPerUnit) Assert.AreEqual(16.95, Product2.UnitPrice) Assert.AreEqual(10, Product2.UnitsInStock) Assert.AreEqual(20, Product2.UnitsOnOrder) Assert.AreEqual(8, Product2.ReorderLevel) '** Delete the product and verify it does not exist **' mRepository.DeleteProduct(Product3.ID) '** The above will use your delete product by id sproc **' Dim Product4 As Product = mRepository.GetProduct(Product3.ID) Assert.AreEqual(Nothing, Product4) End Sub End Class 

我知道这是一个很长的例子,但是它为数据访问工作提供了一个可重用的类,另外还有一个用于testing的可重用类,所以我不必一遍又一遍地执行安装/拆卸工作;)

你尝试过DBUnit吗? 它的目的是unit testing你的数据库,只是你的数据库,而不需要通过你的C#代码。

如果你考虑一下unit testing往往会促进的代码types:小的高度内聚和低耦合的例程,那么你应该几乎能够看到问题的至less一部分。

在我愤世嫉俗的世界里,存储过程是RDBMS世界长期以来试图说服你把业务处理转移到数据库中的一部分,当你认为服务器许可成本往往与处理器数量有关时,这是有意义的。 你在数据库中运行的东西越多,它们就越多。

但是我觉得你实际上更关心性能,这根本就不是unit testing的保留。 unit testing应该是相当primefaces的,旨在检查行为而不是性能。 在这种情况下,您几乎肯定会需要生产类加载来检查查询计划。

我认为你需要一个不同的testing环境。 我会build议一个最简单的生产副本,假设安全不是问题。 然后,对于每个候选版本,您从以前的版本开始,使用您的发布过程进行迁移(这将使这些testing成为一个副作用)并运行您的计时。

就是这样

testing存储过程的关键是编写一个脚本,用预先规划的数据填充空白数据库,以便在调用存储过程时产生一致的行为。

我必须投票支持存储过程,并将我的(和大多数DBA)认为它所属的业务逻辑放在数据库中。

我知道我们作为软件工程师需要用我们最喜欢的语言编写的美妙的重构代码来包含我们所有的重要逻辑,但是大批量系统的性能和数据完整性的关键性质要求我们做出一些妥协。 Sql代码可能是丑陋的,重复的,而且很难testing,但是我无法想象在不完全控制查询devise的情况下调整数据库的难度。

我经常被迫完全重新devise查询,包括对数据模型的更改,以使事情在可接受的时间内运行。 有了存储过程,我可以确保这些更改对调用者来说是透明的,因为存储过程提供了如此出色的封装。

我假设你想在MSSQLunit testing。 看着DBUnit,它对MSSQL的支持有一些限制。 它不支持NVarChar例如。 这里有一些真正的用户和他们与DBUnit的问题。

好问题。

我也有类似的问题,而且我已经走上了阻力最小的路(对我来说,无论如何)。

还有其他一些解决scheme,其他人已经提到。 他们中的许多人更好/更纯粹/更适合他人。

我已经使用Testdriven.NET/MbUnit来testing我的C#,所以我只是将testing添加到每个项目来调用该应用程序使用的存储过程。

我知道我知道。 这听起来很糟糕,但我需要的是通过一些testing来开始,然后从那里出发。 这种方法意味着虽然我的覆盖率很低,但我正在testing一些存储过程,同时testing将调用它们的代码。 这有一些逻辑。

我和原来的海报完全一样。 这涉及到性能和可testing性。 我偏向于可testing性(使其工作,使其正确,快速),这表明保持数据库的业务逻辑。 数据库不仅缺乏像Java这样的语言中的testing框架,代码因子分解结构以及代码分析和导航工具,而且高度考虑因素的数据库代码也很慢(高度分解Java代码的地方)。

但是,我承认数据库集处理的力量。 如果使用得当,SQL可以用很less的代码来完成一些非常强大的function。 所以,即使我仍然尽我所能对它进行unit testing,我仍然可以使用数据库中的一些基于集合的逻辑。

在一个相关的说明中,看起来很长和程序性的数据库代码往往是其他事情的一个症状,我认为这样的代码可以转换成可testing的代码,而不会导致性能下降。 理论上说,这样的代码通常代表了定期处理大量数据的批处理过程。 如果这些批处理过程被转换成更小的实时业务逻辑块,并且每当input数据发生改变时,这个逻辑就可以在中间层(可以被testing的地方)运行,而不会影响性能工作是以小块实时完成的)。 作为一个副作用,这也消除了批处理error handling的长反馈循环。 当然,这种方法在所有情况下都不起作用,但在某些情况下可能起作用。 另外,如果你的系统中有大量的这种不可testing的批处理数据库代码,救援之路可能是漫长而艰难的。 因人而异。

但是我觉得你实际上更关心性能,这根本就不是unit testing的保留。 unit testing应该是相当primefaces的,旨在检查行为而不是性能。 在这种情况下,您几乎肯定会需要生产类加载来检查查询计划。

我认为这里有两个截然不同的testing领域:性能和存储过程的实际逻辑。

我举了过去testingdb性能的例子,谢天谢地,我们已经达到了性能足够好的地步。

我完全同意,数据库中所有业务逻辑的情况是不好的,但是在我们大多数开发人员join公司之前,这是我们inheritance的东西。

但是,现在我们正在为我们的新function采用Web服务模型,并且尽量避免存储过程,将逻辑保留在C#代码中,并将SQLCommands放在数据库中(尽pipelinq现在是首选的方法)。 现在还有一些SP的用法,这就是为什么我想回顾性地testing它们。

您还可以尝试Visual Studio for Database Professionals 。 主要是关于变更pipe理,还有生成testing数据和unit testing的工具。

这是非常昂贵的寿。

我们使用DataFresh来回滚每个testing之间的变化,然后testingsprocs是相对容易的。

代码覆盖工具仍然不足。

我做穷人的unit testing。 如果我很懒,testing只是一些有效的调用,可能有问题的参数值。

 /* --setup Declare @foo int Set @foo = (Select top 1 foo from mytable) --test execute wish_I_had_more_Tests @foo --look at rowcounts/look for errors If @@rowcount=1 Print 'Ok!' Else Print 'Nokay!' --Teardown Delete from mytable where foo = @foo */ create procedure wish_I_had_more_Tests as select.... 

只有从存储过程中删除逻辑并将其重新实现为LINQ查询时,LINQ才会简化它。 这肯定会更健壮,更容易testing。 但是,这听起来像你的要求将排除这一点。

TL; DR:你的devise有问题。

我们unit testing调用SP的C#代码。
我们已经build立脚本,创build干净的testing数据库。
我们在testing夹具中附着和分离的更大的。
这些testing可能需要几个小时,但我认为这是值得的。

一个重新分解代码的选项(我会承认一个丑陋的黑客)将通过CPP(C预处理器)M4(从未尝试过)或类似的东西来生成它。 我有一个这样做的项目,它实际上是可行的。

唯一我认为可能有效的情况是1)作为KLOC +存储过程的一种替代方法,2)这就是我的情况,当项目的要点是看你能推多less技术(疯狂)。

好家伙。 sprocs不适合(自动)unit testing。 我通过在t-sqlbatch file中编写testing并手动检查打印语句和结果的输出来对我的复杂sprocs进行sorting“unit testing”。

unit testing任何types的数据相关的编程的问题是,你必须有一个可靠的testing数据集开始。 很多也取决于存储过程的复杂性和它的function。 对于修改很多表格的非常复杂的过程,unit testing自动化将非常困难。

一些其他的海报已经注意到一些简单的手动自动化testing方法,以及一些可以用于SQL Server的工具。 在Oracle方面,PL / SQL大师Steven Feuerstein为PL / SQL存储过程(称为utPLSQL)开发了免费的unit testing工具。

但是,他放弃了这一努力,然后使用Quest的PL / SQL代码testing工具商业化。 Quest提供免费下载试用版。 我快要尝试一下了; 我的理解是,在build立一个testing框架的时候,它很好的照顾了开销,这样你就可以专注于testing本身,并且保持testing,所以你可以在回归testing中重用它们,这是一个很大的好处。testing驱动开发。 另外,它应该不仅仅是检查一个输出variables,而且还提供了validation数据变化的条件,但是我仍然需要仔细观察自己。 我认为这个信息对Oracle用户可能是有价值的。