我们发现为 C#/C++ 代码编写的单元测试确实得到了回报。但我们的存储过程中仍然有数千行业务逻辑,只有当我们的产品向大量用户推出时,它们才会真正受到愤怒的测试。

更糟糕的是,由于在 SP 之间传递临时表时性能受到影响,其中一些存储过程最终变得非常长。这阻碍了我们重构以使代码更简单。

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

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

我的问题的第二部分是使用 linq 进行单元测试是否会更容易?

我在想,您可以简单地创建测试对象的集合,并在“linq to object”情况下测试您的 linq 代码,而不是必须设置测试数据表?(我是 linq 的新手,所以不知道这是否有效)

有帮助吗?

解决方案

不久前我遇到了同样的问题,发现如果我为数据访问创建一个简单的抽象基类,允许我注入连接和事务,我可以对我的存储过程进行单元测试,看看它们是否完成了我在 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 周期,以确保所有存储过程都完成其工作,并且您的 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

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

其他提示

你有没有尝试过 数据库单元?它旨在对您的数据库进行单元测试,并且仅对您的数据库进行单元测试,而无需检查您的 C# 代码。

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

在我愤世嫉俗的世界中,存储过程是 RDBMS 世界长期以来试图说服您将业务处理转移到数据库中的一部分,当您考虑到服务器许可成本往往与处理器数量等因素相关时,这是有道理的。您在数据库中运行的内容越多,它们从您那里获得的收益就越多。

但我的印象是您实际上更关心性能,这根本不是单元测试的范围。单元测试应该是相当原子的,旨在检查行为而不是性能。在这种情况下,您几乎肯定需要生产级加载才能检查查询计划。

我认为您需要不同级别的测试环境。我建议使用最简单的生产副本,假设安全性不是问题。然后,对于每个候选版本,您从以前的版本开始,使用您的发布程序进行迁移(这将给这些版本带来良好的测试作为副作用)并运行您的计时。

类似的事情。

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

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

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

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

我假设您想要在 MSSQL 中进行单元测试。查看 DBUnit,它对 MSSQL 的支持存在一些限制。例如,它不支持 NVarChar。 以下是一些真实用户以及他们使用 DBUnit 时遇到的问题。

好问题。

我也有类似的问题,并且我采取了阻力最小的道路(无论如何对我来说)。

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

我已经在使用 Testdriven.NET/MbUnit 来测试我的 C#,因此我只需向每个项目添加测试来调用该应用程序使用的存储过程。

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

我的情况与原始海报完全相同。这归结为性能与可测试性。我偏向于可测试性(使其工作、使其正确、使其快速),这建议将业务逻辑保留在数据库之外。数据库不仅缺乏 Java 等语言中的测试框架、代码分解构造以及代码分析和导航工具,而且高度分解的数据库代码也很慢(而高度分解的 Java 代码则不然)。

然而,我确实认识到数据库集处理的力量。如果使用得当,SQL 可以用很少的代码做一些非常强大的事情。因此,我同意数据库中存在一些基于集合的逻辑,尽管我仍然会尽一切努力对其进行单元测试。

与此相关的是,似乎非常长的过程数据库代码通常是其他东西的症状,我认为这样的代码可以转换为可测试的代码,而不会导致性能下降。从理论上讲,此类代码通常代表定期处理大量数据的批处理过程。如果将这些批处理过程转换为每当输入数据更改时运行的较小的实时业务逻辑块,则该逻辑可以在中间层(可以对其进行测试)上运行,而不会影响性能(因为该工作是实时以小块的形式完成的)。作为副作用,这也消除了批处理错误处理的长反馈循环。当然,这种方法并非在所有情况下都有效,但在某些情况下可能有效。此外,如果您的系统中存在大量此类无法测试的批处理数据库代码,那么拯救之路可能是漫长而艰巨的。YMMV。

但我的印象是您实际上更关心性能,这根本不是单元测试的范围。单元测试应该是相当原子的,旨在检查行为而不是性能。在这种情况下,您几乎肯定需要生产级加载来检查查询计划。

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

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

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

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

你也可以尝试 面向数据库专业人员的 Visual Studio. 。它主要涉及变更管理,但也具有用于生成测试数据和单元测试的工具。

它是相当昂贵的。

我们用 数据新鲜 要回滚每个测试之间的更改,那么测试存储过程相对容易。

仍然缺乏的是代码覆盖率工具。

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

/*

--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 才会简化此操作。这肯定会更加健壮并且更容易测试。但是,听起来您的要求会排除这种情况。

长话短说:你的设计有问题。

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

重构代码的一个选择(我承认这是一个丑陋的黑客)是通过 CPP(C 预处理器)M4(从未尝试过)等生成它。我有一个项目就是这样做的,而且它实际上基本上是可行的。

我认为唯一可能有效的情况是 1) 作为 KLOC+ 存储过程的替代方案,2) 这是我的情况,项目的重点是看看您可以将一项技术推向多远(疯狂)。

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

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

其他一些发帖者指出了一些自动手动测试它们的简单方法,以及一些可以与 SQL Server 一起使用的工具。在 Oracle 方面,PL/SQL 大师 Steven Feuerstein 为 PL/SQL 存储过程开发了一个名为 utPLSQL 的免费单元测试工具。

然而,他放弃了这一努力,然后将 Quest 的 PL/SQL 代码测试器商业化。Quest 提供免费下载试用版。我正准备尝试一下;我的理解是,它擅长处理设置测试框架的开销,以便您可以只关注测试本身,并且它保留测试,以便您可以在回归测试中重用它们,这是测试驱动开发。此外,它应该不仅仅擅长检查输出变量,并且确实具有验证数据更改的功能,但我仍然需要自己仔细看看。我认为此信息可能对 Oracle 用户有价值。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top