我们发现我们为C#/ C ++代码编写的单元测试确实得到了回报。 但是我们仍然在存储过程中拥有数千行业务逻辑,当我们的产品推广到大量用户时,它们才真正得到了真正的测试。
更糟糕的是,这些存储过程中的一些最终会很长,因为在SP之间传递临时表时性能会受到影响。这使我们无法进行重构以使代码更简单。
我们已经尝试围绕一些关键存储过程构建单元测试(主要是测试性能),但是发现为这些测试设置测试数据真的很难。例如,我们最终复制测试数据库。除此之外,测试最终对变化非常敏感,甚至是对存储过程的最小变化。或表需要对测试进行大量更改。因此,由于这些数据库测试间歇性地失败导致许多构建中断,我们只需将它们从构建过程中拉出来。
所以,我的问题的主要部分是:有没有人成功为他们的存储过程编写单元测试?
我的第二部分问题是linq的单元测试是否更容易?
我在想,不是必须设置测试数据表,您可以简单地创建一组测试对象,并在“linq to objects”情况下测试您的linq代码? (我对linq来说是全新的,所以不知道这是否会起作用)
答案 0 :(得分:12)
我前一段时间遇到了同样的问题,发现如果我为数据访问创建了一个简单的抽象基类,允许我注入连接和事务,我可以对我的sprocs进行单元测试,看看他们是否完成了我要求他们执行然后回滚的SQL,因此没有任何测试数据留在数据库中。
这比通常的“运行脚本来设置我的测试数据库,然后在测试运行后对垃圾/测试数据进行清理”感觉更好。这也感觉更接近于单元测试,因为这些测试可以单独运行,而且在运行这些测试之前,“数据库中的所有内容都需要'才这样'。”
以下是用于数据访问的抽象基类的片段
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
现在,在您的单元测试中,您还可以从一个非常简单的基类继承您的设置/回滚工作 - 或者保持每个单元测试
下面是我使用的简单测试基类
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
最后 - 以下是使用该测试基类的简单测试,该测试基类显示如何测试整个CRUD周期以确保所有sprocs完成其工作并且您的ado.net代码执行左右正确映射
我知道这不会测试上面数据访问示例中使用的“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
我知道这是一个很长的例子,但它有助于为数据访问工作提供可重用的类,而且还有另一个可重用的类用于我的测试,因此我不必一次又一次地进行设置/拆卸工作;)
答案 1 :(得分:10)
你试过DBUnit吗?它旨在对您的数据库和数据库进行单元测试,而无需通过C#代码。
答案 2 :(得分:6)
如果你考虑单元测试倾向于推广的代码类型:小的高度内聚和低耦合的例程,那么你几乎应该能够看到问题的至少部分可能在哪里。
在我愤世嫉俗的世界中,存储过程是RDBMS世界长期以来试图说服您将业务处理转移到数据库中的一部分,这在您考虑服务器许可成本往往与处理器之类的事情有关时是有意义的计数。您在数据库中运行的内容越多,它们就越多。
但是我得到的印象是你实际上更关心性能,这根本不是单元测试的保留。单元测试应该是相当原子的,旨在检查行为而不是性能。在这种情况下,您几乎肯定需要生产类加载才能检查查询计划。
我认为您需要一个不同类别的测试环境。我建议将生产副本作为最简单的,假设安全性不是问题。然后,对于每个候选版本,您从先前版本开始,使用您的发布过程进行迁移(这将为那些良好的测试提供副作用)并运行您的计时。
类似的东西。
答案 3 :(得分:6)
测试存储过程的关键是编写一个脚本,该脚本使用预先计划好的数据填充空白数据库,以便在调用存储过程时产生一致的行为。
我必须投票支持存储过程,并将业务逻辑放在我(以及大多数DBA)认为属于数据库的地方。
我知道我们作为软件工程师需要用我们最喜欢的语言编写的精美重构代码来包含我们所有重要的逻辑,但是大批量系统中的性能现实以及数据完整性的关键性质要求我们做出一些妥协。 Sql代码可能很难看,重复且难以测试,但我无法想象在没有完全控制查询设计的情况下调整数据库的难度。
我经常被迫完全重新设计查询,包括对数据模型的更改,以便在可接受的时间内运行。使用存储过程,我可以确保更改对调用者是透明的,因为存储过程提供了如此出色的封装。
答案 4 :(得分:4)
我假设你想在MSSQL中进行单元测试。看看DBUnit,它对MSSQL的支持有一些限制。例如,它不支持NVarChar。 Here are some real users and their problems with DBUnit.
答案 5 :(得分:4)
好问题。
我有类似的问题,我采取了阻力最小的道路(对我来说,无论如何)。
还有一些其他解决方案,其他人提到过。其中许多更好/更纯净/更适合其他人。
我已经使用Testdriven.NET/MbUnit来测试我的C#,所以我只是为每个项目添加测试来调用该应用程序使用的存储过程。
我知道,我知道。这听起来很糟糕,但我需要的是通过一些测试开始,并从那里开始。这种方法意味着虽然我的覆盖率很低,但我正在测试一些存储过程,因为我正在测试将调用它们的代码。这有一些逻辑。
答案 6 :(得分:3)
我和原版海报完全一样。它归结为性能与可测试性。我的偏见是可测试性(使其工作,使其正确,使其快速),这表明将业务逻辑保持在数据库之外。数据库不仅缺乏测试框架,代码分解构造,以及Java等语言中的代码分析和导航工具,而且高度因素化的数据库代码也很慢(高度依赖的Java代码不是这样)。
但是,我确实认识到数据库集处理的强大功能。如果使用得当,SQL可以用很少的代码做一些非常强大的东西。所以,我可以使用基于集合的逻辑生活在数据库中,即使我仍然会尽我所能对其进行单元测试。
在相关的说明中,似乎很长且程序性的数据库代码通常是其他东西的症状,我认为这样的代码可以转换为可测试代码而不会导致性能损失。理论上,这种代码通常代表定期处理大量数据的批处理过程。如果将这些批处理过程转换为更小的实时业务逻辑块,只要输入数据发生变化就会运行,这个逻辑可以在中间层(可以测试它)上运行,而不会影响性能(因为工作以小块实时完成。作为副作用,这也消除了批处理错误处理的长反馈循环。当然,这种方法并不适用于所有情况,但它可能在某些情况下有效。此外,如果您的系统中存在大量此类不可测试的批处理数据库代码,那么拯救之路可能是漫长而艰巨的。 YMMV。
答案 7 :(得分:2)
但是我得到的印象是你实际上更关心性能,这根本不是单元测试的保留。单元测试应该是相当原子的,旨在检查行为而不是性能。在这种情况下,您几乎肯定需要生产类加载才能检查查询计划。
我认为这里有两个截然不同的测试区域:性能和存储过程的实际逻辑。
我给出了过去测试db性能的示例,幸运的是,我们已经达到了性能足够好的程度。
我完全同意数据库中所有业务逻辑的情况都很糟糕,但这是我们在大多数开发人员加入公司之前继承的东西。
但是,我们现在正在为我们的新功能采用Web服务模型,并且我们一直在尝试尽可能地避免存储过程,将逻辑保留在C#代码中并在数据库中触发SQLCommands(尽管linq现在将是首选的方法)。现有的SP还有一些用途,这就是为什么我在考虑对它们进行回顾性单元测试的原因。
答案 8 :(得分:2)
您也可以尝试Visual Studio for Database Professionals。它主要是关于变更管理,但也有生成测试数据和单元测试的工具。
这是非常昂贵的。
答案 9 :(得分:1)
我们使用DataFresh在每次测试之间回滚更改,然后测试sprocs相对容易。
还缺少代码覆盖工具。
答案 10 :(得分:1)
我做穷人的单位测试。如果我很懒,那么测试只是一些有效的调用,可能存在问题参数值。
/*
--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....
答案 11 :(得分:0)
只有从存储过程中删除逻辑并将其重新实现为linq查询时,LINQ才会简化此操作。肯定会哪个更强大,更容易测试。但是,听起来您的要求会妨碍这一点。
TL; DR:你的设计有问题。
答案 12 :(得分:0)
我们对调用SP的C#代码进行单元测试 我们已经构建了脚本,创建了干净的测试数据库 在测试夹具中我们附着和分离的更大的 这些测试可能需要数小时,但我认为这是值得的。
答案 13 :(得分:0)
重新计算代码的一个选项(我承认一个丑陋的黑客)将通过CPP(C预处理器)M4(从未尝试过)等生成它。我有一个正在做的项目,它实际上是可行的。
我认为可能有效的唯一情况是1)作为KLOC +存储过程的替代方案和2)这是我的情况,当项目的要点是看你能推多远(变成疯了)一项技术。
答案 14 :(得分:0)
哦,伙计。 sprocs不适合(自动化)单元测试。我通过在t-sql批处理文件中编写测试并手动检查print语句的输出和结果,对我的复杂sprocs进行“单元测试”。
答案 15 :(得分:0)
单元测试任何类型的数据相关编程的问题在于,您必须拥有一组可靠的测试数据。很多还取决于存储过程的复杂性及其作用。对于修改了许多表的非常复杂的过程,自动化单元测试将非常困难。
其他一些海报已经注意到一些简单的方法来自动手动测试它们,还有一些可以与SQL Server一起使用的工具。在Oracle方面,PL / SQL大师Steven Feuerstein开发了一个名为utPLSQL的PL / SQL存储过程的免费单元测试工具。
但是,他放弃了这项工作,然后使用Quest的Code Tester进行PL / SQL商业广告。 Quest提供免费下载的试用版。我快要试试了;我的理解是,它很好地处理了设置测试框架的开销,以便您可以专注于测试本身,并保留测试,以便您可以在回归测试中重用它们,这是一个很好的好处。测试驱动开发。此外,它应该不仅仅是检查输出变量,还有确认数据更改的条款,但我仍然需要仔细研究一下。我认为这些信息可能对Oracle用户有价值。