有没有人曾在单元测试SQL存储过程的任何成功?(Has anyone had any success

2019-06-28 04:58发布

我们发现,我们已经为我们的C#/ C ++代码编写单元测试确实得到了回报。 但是,我们仍然有成千上万的业务逻辑线在存储过程中,只有真正愤怒时,我们的产品推广到大量用户的得到测试。

是什么让这个糟糕的是,一些这些存储过程最终通过SP之间的临时表时,是因为对性能的影响非常长。 这使得我们无法重构,使代码更简单。

我们已经在建筑周围的一些我们的关键存储过程的单元测试,试了好几次(主要测试性能),但已经发现,这些测试设置测试数据真的很难。 例如,我们最终周围测试数据库复制。 除此之外,测试最终被一个存储过程真正敏感的变化,即使是最小的变化。 或表需要大量的变化的测试。 因此,许多后建立打破由于这些数据库测试间断性地失效,我们刚刚不得不拉出来的构建过程。

所以,我的问题的主要部分是:有没有人成功写入单元测试他们的存储过程?

我的问题的第二部分是单元测试会否/与LINQ更容易吗?

我在想,而不是建立测试数据表,你可以简单地创建测试对象的集合,并在测试你的LINQ代码“LINQ到对象”的情况? (我是一个完全陌生的LINQ所以不知道这是否会甚至在所有的工作)

Answer 1:

我遇到了同样的问题而回,结果发现,如果我创建了数据访问,让我注入一个连接和交易,我可以单元测试我的存储过程,看看他们是否的确在SQL工作简单的抽象基类,我要求他们做什么,然后回滚所以没有测试数据在数据库中留下。

这感觉比以往更好“运行脚本设置我的测试数据库,再经过测试运行做好垃圾/测试数据的清理”。 这也觉得更接近单元测试,因为这些测试可以独自瓦特/出具有“一切都在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

现在在你的单元测试您还可以从一个非常简单的基类,做你的设置/回滚工作继承-或保持这种对每个单元测试的基础

下面是我用简单的测试基类

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周期,以确保所有的存储过程做他们的工作和你的ado.net代码不正确的左右映射一个简单的测试

我知道这不会测试上面的数据访问示例中使用的“spGetProducts”的存储过程,但你应该看到这个单元测试方法的存储过程背后的力量

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

我知道这是一个长期的例子,但它有助于对数据访问的工作为我的测试可重复使用的类,还有一个可重用的类,所以我没得一遍又一遍地做安装/拆卸工作再次;)



Answer 2:

您是否尝试过DBUnit的 ? 它的设计单元测试您的数据库,只是你的数据库,而无需经过你的C#代码。



Answer 3:

如果你想的那种代码单元测试倾向于促进:小高内聚和低耦合的程序,那么你应该非常能够看到问题的至少一部分可能。

在我的愤世嫉俗的世界,存储过程是RDBMS世界的长期努力说服您将业务处理转移到数据库中,当你考虑到服务器许可证成本往往涉及到的东西像处理器数量这是有意义的一部分。 您在数据库中运行更多的东西,就越让您的意见。

但我得到你实际上更关心的是性能的印象,这是不是真的单元测试的保留都没有。 单元测试应该是相当原子弹,是为了检查行为,而不是性能。 而在这种情况下,你几乎肯定会需要才能生产一流的负载,用于检查查询计划。

我想你需要不同类别的测试环境。 我建议生产为简单的拷贝,假设安全不是问题。 然后,对于每一个候选版本,你开始与以前的版本,迁移使用您的发布程序(这将给那些良好的检测作为一个副作用)和运行时间安排。

类似的东西。



Answer 4:

测试存储过程的关键是写一个填充空白数据库与计划提前出局导致一致行为的存储过程调用时数据的脚本。

我已经把我的票在很大程度上有利于存储过程,并把你的业务逻辑,我(和大多数DBA)认为它属于,在数据库中。

我知道,我们的软件工程师要精美重构的代码,写在我们最喜欢的语言,包含了所有我们的重要逻辑的,但在高容量系统的性能的实际情况和数据完整性的关键性质,需要我们做出一些妥协。 SQL代码可以丑,重复和难以测试,但不必在查询的设计完全控制我无法想象在调优数据库的难度。

我常常被迫完全重新设计的查询,包括对数据的更改模型,把事情在一个可接受的时间量运行。 在存储过程中,我可以保证,这些变化将是透明的呼叫者,因为在存储过程提供了这种优异的封装。



Answer 5:

我假设你想在MSSQL单元测试。 看着DBUnit的有在MSSQL它的支持一定的局限性。 它不支持的NVarChar的实例。 下面是一些真实的用户以及他们与DBUnit的问题。



Answer 6:

好问题。

我有类似的问题,我已经采取了阻力最小的路径(对我来说,反正)。

还有很多其他的解决方案,其他人mentionned的。 他们中许多人都更好/更纯/更适合别人。

我已经使用Testdriven.NET/MbUnit测试我的C#,所以我只是简单地添加测试,每个项目调用由该应用程序所使用的存储过程。

我知道我知道。 这听起来很可怕,但我需要的是一些测试,以离开地面,并从那里走。 这种方法意味着,尽管我的覆盖率很低,我在同一时间测试一些存储的特效,因为我测试将被调用它们的代码。 有一些逻辑到这一点。



Answer 7:

我在完全相同的情况,因为原来的海报。 它归结为性能与可测性。 我的偏见是对可测试性(使工作,使之正确,使之快),这表明保持业务逻辑从数据库中。 数据库不仅缺乏测试框架,代码保理构造和代码分析和导航工具,如Java语言中,但高度分解的数据库代码也慢(其中高分解Java代码是不是)。

不过,我承认数据库集处理能力。 如果使用得当,SQL可以做一些非常强大的东西,用很少的代码。 所以,我确定了一些基于集合的逻辑居住在数据库中,即使我仍然会尽我所能,以单元测试。

与此相关的,似乎很长,程序数据库代码往往是别的症状,我认为这样的代码可以被转换为可测试的代码,而不会产生性能损失。 该理论认为,这样的代码经常表示周期性地处理大量数据的批处理。 如果这些批处理进程是不以性能损失(因为要转换成运行每当输入数据发生变化,这种逻辑可以在中间层(它可以测试)来运行,实时业务逻辑的小块这项工作是在实时小块)来完成。 作为一个副作用,这也消除了批处理过程的错误处理长反馈环路。 当然,这种方法不会在所有情况下工作,但它可能在某些工作。 此外,如果有吨这种不可测的批处理的数据库代码在你的系统中,救赎之路可能是漫长而艰苦的。 因人而异。



Answer 8:

但我得到你实际上更关心的是性能的印象,这是不是真的单元测试的保留都没有。 单元测试应该是相当原子弹,是为了检查行为,而不是性能。 而在这种情况下,你几乎肯定会需要才能生产一流的负载,用于检查查询计划。

我觉得有两个截然不同的检测区域位置:性能和存储过程的实际逻辑。

我给在过去的测试数据库性能的例子,幸运的是,我们已经达到了一个点里的表现不够好。

我完全同意,与数据库中的所有的业务逻辑的情况是不好的,但它的东西,我们已经从之前我们大部分的开发者加入该公司继承。

然而,我们现在采取的新功能的Web服务模型,我们一直在试图避免存储过程尽可能,保持逻辑的C#代码和烧成SQLCommands在数据库(虽然LINQ现在会是优选的方法)。 还有一些使用这就是为什么我在想回顾单元测试他们现有的SP的。



Answer 9:

您也可以尝试的Visual Studio数据库专业人士 。 它的主要变革管理,但也有生成测试数据和单元测试工具。

这是非常昂贵的寿。



Answer 10:

我们使用DataFresh回滚每次测试之间的变化,然后测试存储过程是比较容易的。

什么是仍然缺乏是代码覆盖工具。



Answer 11:

我做穷人的单元测试。 如果我懒惰,测试只是一对夫妇的有效调用与潜在问题的参数值。

/*

--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....


Answer 12:

只有当你从你的存储过程中删除逻辑和重新实现它作为LINQ查询LINQ将简化这一点。 这将是更强大和更易于测试,绝对。 然而,这听起来像你的要求将排除这一点。

TL; DR:你的设计有问题。



Answer 13:

我们单元测试调用SP上的C#代码。
我们必须建立脚本,创建干净的测试数据库。
而更大的人,我们重视和测试夹具中分离。
这些测试可能需要数小时,但我认为它`值得的。



Answer 14:

一个选项重新因子的代码(我承认一个丑陋的黑客攻击)是通过CPP(C预处理器)来生成它M4(从来没有尝试过)等。 我有一个只是在做一个项目,它实际上主要是可行的。

唯一的情况下,我认为这可能是有效的是1)作为替代KLOC +存储过程和2),这是我的情况下,在该项目的重点是看多远,你可以(进入疯狂)推技术。



Answer 15:

好家伙。 存储过程不适合自己至(自动的)的单元测试。 我有点-“单元测试”我在T-SQL批处理文件和手工检查打印报表和结果的输出编写测试复杂的存储过程。



Answer 16:

单元测试任何类型的数据相关的编程的问题是,你必须有一个可靠的测试数据集的开始。 很多还依赖于存储的过程的复杂性和它做什么。 这将是非常困难的一个非常复杂的过程,修改许多表自动化单元测试。

其他一些海报已经注意到一些简单的方法来手动自动测试它们,也有一些工具,你可以使用SQL Server。 在Oracle方面,PL / SQL大师史蒂芬斯坦曾在一个免费的单元测试工具,PL / SQL存储过程称为utPLSQL。

然而,他放弃了这一努力,然后又与商业Quest的代码测试仪PL / SQL。 任务提供了一个免费下载的试用版。 我热衷于尝试它的边缘; 我的理解是,它善于在建立一个测试框架以架空的照顾,让您可以只专注于测试本身,它不断的测试,这样你就可以在回归测试重用他们来说,最大的好处一个测试驱动开发。 此外,它应该是善于不仅仅是检查的输出变量,确实有规定,用于验证数据的变化更多,但我还是要仔细看看自己。 我想这个信息可能是价值的Oracle用户。



文章来源: Has anyone had any success in unit testing SQL stored procedures?