有没有人在单元测试SQL存储过程方面取得任何成功?

有没有人在单元测试SQL存储过程方面取得任何成功?

Has anyone had any success in unit testing SQL stored procedures?

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

更糟糕的是,由于在SP之间传递临时表时会降低性能,因此某些存储过程最终会变得很长。这使我们无法重构以简化代码。

我们已经尝试过围绕一些关键存储过程构建单元测试(主要是测试性能),但是发现为这些测试设置测试数据确实很困难。例如,我们最终围绕测试数据库进行复制。除此之外,测试最终对更改甚至是对存储过程的最小更改都非常敏感。或表格需要对测试进行大量更改。因此,在由于这些数据库测试间歇性失败而导致许多构建中断之后,我们只需要将它们退出构建过程即可。

因此,我的问题的主要部分是:是否有人成功为其存储过程编写了单元测试?

我的问题的第二部分是,使用linq是否会简化单元测试?

我在想,您不必建立测试数据表,而只需创建测试对象的集合,并在" linq to objects"情况下测试linq代码? (我对linq完全陌生,所以根本不知道这是否可行)


不久前,我遇到了同样的问题,发现如果我为数据访问创建了一个简单的抽象基类,该类允许我注入连接和事务,则可以对存储过程进行单元测试,以查看它们是否在SQL中完成了工作要求他们这样做,然后回滚,以便所有测试数据都不会留在数据库中。

这感觉比通常的"运行脚本来设置我的测试数据库,然后在测试运行之后清理垃圾/测试数据"感觉更好。这也感觉更接近于单元测试,因为这些测试可以单独运行,而无需有很多"在运行这些测试之前,数据库中的所有内容都必须"如此"。

这是用于数据访问的抽象基类的摘要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

现在,在单元测试中,您还可以从一个非常简单的基类中继承该基类,以进行设置/回滚工作-或在每个单元测试的基础上进行保留

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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"存储过程,但是您应该看到此方法进行单元测试存储过程的强大功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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

我知道这是一个很长的示例,但它有助于为数据访问工作提供一个可重用的类,并为我的测试提供另一个可重用的类,因此我不必一遍又一遍地进行设置/拆卸工作;)


您是否尝试过DBUnit?它旨在对数据库和仅对数据库进行单元测试,而无需通过C#代码。


测试存储过程的关键是编写一个脚本,该脚本使用预先计划好的数据填充空白数据库,从而在调用存储过程时产生一致的行为。

我必须投赞成票的方式来大力支持存储过程,并将您的业务逻辑放置在数据库中我(以及大多数DBA)认为属于的位置。

我知道我们作为软件工程师想要用我们喜欢的语言编写的精美的重构代码包含我们所有的重要逻辑,但是大容量系统中的性能现实和数据完整性的关键性质要求我们做出一些妥协。 Sql代码可能很丑陋,重复且难以测试,但是我无法想象没有完全控制查询设计的情况下调优数据库的困难。

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


如果您考虑单元测试倾向于推广的代码类型:小型的高内聚和低耦合的例程,那么您应该几乎能够看到问题的至少一部分在哪里。

在我愤世嫉俗的世界中,存储过程是RDBMS世界长期尝试说服您将业务处理转移到数据库中的一部分,当您认为服务器许可成本往往与处理器数量有关时,这是有道理的。您在数据库中运行的东西越多,它们从您身上获得的收益就越多。

但是我得到的印象是,您实际上实际上更关心性能,而这实际上并不是单元测试的全部保留。单元测试应该是相当原子的,旨在检查行为而不是性能。在那种情况下,您几乎肯定会需要生产级负载才能检查查询计划。

我认为您需要另一类测试环境。我建议生产副本是最简单的,假设安全性不是问题。然后,对于每个候选发行版,您将从先前版本开始,使用发行过程进行迁移(这将给您带来很好的测试效果),并运行计时。

这样的东西。


好问题。

我有类似的问题,并且我选择了阻力最小的方法(无论如何对我而言)。

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

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

我知道我知道。这听起来很糟糕,但是我需要做的是进行一些测试,然后再进行测试。这种方法意味着尽管我的覆盖率很低,但是我正在测试将要调用它们的代码的同时测试了一些存储的proc。这有一些逻辑。


我假设您要在MSSQL中进行单元测试。从DBUnit的角度来看,它对MSSQL的支持存在一些限制。例如,它不支持NVarChar。以下是一些实际用户及其使用DBUnit的问题。


我和原始海报的情况完全一样。它归结为性能与可测试性。我的偏向于可测试性(使其正常运行,正确运行,快速运行),这建议将业务逻辑保留在数据库之外。数据库不仅缺少Java之类的语言提供的测试框架,代码分解结构以及代码分析和导航工具,而且高度分解的数据库代码也很慢(没有高度分解的Java代码)。

但是,我确实知道数据库集处理的功能。如果使用得当,SQL可以用很少的代码完成一些功能强大的工作。因此,尽管我将尽我所能来进行单元测试,但我对数据库中存在的一些基于集合的逻辑还可以。

与此相关的是,似乎很长的程序数据库代码通常是其他问题的征兆,我认为可以将此类代码转换为可测试的代码而不会造成性能损失。从理论上讲,此类代码通常代表批处理,这些批处理定期处理大量数据。如果将这些批处理流程转换为每当更改输入数据时便运行的较小的实时业务逻辑块,则可以在中间层(可以对其进行测试)上运行该逻辑,而不会造成性能损失(因为这项工作是实时进行的。作为副作用,这还消除了批处理错误处理中的长反馈循环。当然,这种方法并非在所有情况下都适用,但在某些情况下可能适用。另外,如果您的系统中有大量此类不可测试的批处理数据库代码,那么拯救之路可能会很漫长而艰巨。 YMMV。


But I get the impression you're actually more concerned with performance, which isn't really the preserve of unit testing at all. Unit tests are supposed to be fairly atomic and are intended to check behaviour rather than performance. And in that case you're almost certainly going to need production-class loads in order to check query plans.

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

我给出了过去测试数据库性能的示例,值得庆幸的是,我们已经达到了性能足够好的地步。

我完全同意,数据库中所有业务逻辑的情况都是糟糕的情况,但这是我们在大多数开发人员加入公司之前继承的。

但是,我们现在为新功能采用了Web服务模型,并且我们一直在尝试尽可能避免存储过程,将逻辑保留在C#代码中并在数据库中触发SQLCommand(尽管现在linq首选方法)。现有SP仍然有一些用途,这就是为什么我考虑对它们进行回顾性单元测试。


您也可以尝试使用Visual Studio for Database Professionals。它主要涉及变更管理,还具有用于生成测试数据和单元测试的工具。

太贵了。


我做穷人的单元测试。 如果我很懒,测试只是几个有效的调用,参数值可能有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*

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

我们使用DataFresh来回滚每个测试之间的更改,然后测试存储过程相对容易。

仍然缺少代码覆盖率工具。


好家伙。存储过程不适合(自动)单元测试。通过在t-sql批处理文件中编写测试,并手动检查打印语句的输出和结果,我对复杂的存储程序进行了"单元测试"。


我们对调用SP的C#代码进行单元测试。
我们有构建脚本,可以创建干净的测试数据库。
在测试夹具期间,我们会连接和分离更大的部件。
这些测试可能需要几个小时,但我认为值得。


任何类型的与数据相关的编程的单元测试问题在于,您必须首先拥有一组可靠的测试数据。很大程度上还取决于存储过程的复杂性及其作用。对于修改了许多表的非常复杂的过程,很难使单元测试自动化。

其他一些张贴者指出了一些自动进行手动测试的简单方法,以及一些可用于SQL Server的工具。在Oracle方面,PL / SQL专家Steven Feuerstein致力于开发用于utPLSQL的PL / SQL存储过程的免费单元测试工具。

但是,他放弃了精力,然后使用Quest的PL / SQL代码测试器进行商业化。 Quest提供免费的可下载试用版。我快要尝试了;我的理解是,这样做可以很好地解决建立测试框架的开销,使您可以只关注测试本身,并且可以保留测试,以便可以在回归测试中重用它们,这是该工具的一大优点。测试驱动的开发。此外,它不仅仅要检查输出变量,还应该提供验证数据更改的功能,但是我仍然需要自己仔细研究一下。我认为该信息可能对Oracle用户有价值。


仅当您从存储过程中删除逻辑并将其重新实现为linq查询时,LINQ才会简化此操作。无疑,这将更加健壮并且更容易测试。但是,听起来您的要求会排除这种情况。

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


重构代码的一种选择(我会承认这很丑陋)是通过CPP(C预处理器)M4(从没尝试过)或类似方法生成代码。我有一个项目就是这样做的,实际上它几乎是可行的。

我认为唯一可能适用的情况是1)作为KLOC +存储过程的替代方法,以及2),这就是我的情况,当时项目的重点是看可以将技术推向多远(陷入疯狂)。


推荐阅读