37

C#/C++ コード用に作成した単体テストが本当に効果があることがわかりました。しかし、ストアド プロシージャにはまだ数千行のビジネス ロジックがあり、実際にテストされるのは、製品が多数のユーザーに公開されたときだけです。

さらに悪いことに、SP 間で一時テーブルを渡すときにパフォーマンスが低下するため、これらのストアド プロシージャの一部が非常に長くなることがあります。これにより、コードを単純化するためのリファクタリングが妨げられました。

いくつかの重要なストアド プロシージャを中心に単体テストを作成する試みを何度か行いました (主にパフォーマンスのテスト) が、これらのテスト用にテスト データを設定するのは非常に困難であることがわかりました。たとえば、テスト データベースをコピーすることになります。これに加えて、テストは変更に非常に敏感になり、ストアド プロシージャへの最小の変更でさえも敏感になります。またはテーブルは、テストに大量の変更を必要とします。そのため、これらのデータベース テストが断続的に失敗したために多くのビルドが機能しなくなった後、ビルド プロセスから除外する必要がありました。

私の質問の主な部分は、ストアド プロシージャの単体テストを正常に作成したことがありますか?

私の質問の 2 番目の部分は、単体テストが linq で簡単になるかどうかです。

テスト データのテーブルを設定するのではなく、単純にテスト オブジェクトのコレクションを作成し、「linq to objects」の状況で linq コードをテストできるのではないかと考えていました。(私はlinqにまったく慣れていないので、これがまったく機能するかどうかわかりません)

4

16 に答える 16

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 つ用意するのに役立ちました。そのため、セットアップ/ティアダウン作業を何度も行う必要はありません ;)

于 2008-08-24T17:20:14.487 に答える
10

DBUnitを試しましたか? C# コードを実行する必要なく、データベースとデータベースのみを単体テストするように設計されています。

于 2008-08-15T15:35:02.907 に答える
6

単体テストが助長する傾向があるコードの種類、つまり凝集度が高く、結合度の低い小さなルーチンについて考えてみると、問題の少なくとも一部がどこにあるのかをほぼ把握できるはずです。

私の冷笑的な世界では、ストアド プロシージャは、ビジネス処理をデータベースに移行するよう説得しようとする RDBMS の世界の長年にわたる試みの一部です。これは、サーバー ライセンスのコストがプロセッサ数などに関連する傾向があることを考えると理にかなっています。データベース内で実行するものが多ければ多いほど、彼らはあなたからより多くを稼ぎます。

しかし、あなたは実際にはパフォーマンスにもっと関心を持っているように感じます。これは、実際には単体テストの領域ではありません。単体テストはかなりアトミックであると想定されており、パフォーマンスではなく動作をチェックすることを目的としています。その場合、ほぼ確実に、クエリ プランをチェックするためにプロダクション クラスのロードが必要になります。

別のクラスのテスト環境が必要だと思います。セキュリティが問題にならないと仮定すると、本番環境のコピーを最も単純なものとしてお勧めします。次に、候補リリースごとに、以前のバージョンから開始し、リリース手順を使用して移行し (これにより、副作用として適切なテストが行​​われます)、タイミングを実行します。

そんな感じ。

于 2008-08-15T15:51:15.433 に答える
6

ストアド プロシージャをテストするための鍵は、ストアド プロシージャが呼び出されたときに一貫した動作が得られるように事前に計画されたデータを空のデータベースに入力するスクリプトを作成することです。

ストアド プロシージャを大いに支持し、ビジネス ロジックをデータベース内の私 (およびほとんどの DBA) が属すると考える場所に配置することに投票する必要があります。

私たちソフトウェア エンジニアは、お気に入りの言語で書かれた美しくリファクタリングされたコードに重要なロジックをすべて含めたいと考えていますが、大容量システムでのパフォーマンスの現実とデータの整合性の重要な性質により、ある程度の妥協が必要になります。 . SQL コードは見苦しく、繰り返しが多く、テストが難しい場合がありますが、クエリの設計を完全に制御せずにデータベースをチューニングすることの難しさは想像できません。

クエリを完全に再設計し、データ モデルに変更を加え、許容できる時間内に実行できるようにしなければならないことがよくあります。ストアド プロシージャを使用すると、変更が呼び出し元に対して透過的であることを保証できます。これは、ストアド プロシージャが非常に優れたカプセル化を提供するためです。

于 2008-08-15T17:27:51.963 に答える
4

MSSQL での単体テストが必要であると想定しています。DBUnit を見ると、MSSQL のサポートにはいくつかの制限があります。たとえば、NVarChar はサポートされていません。 以下は、実際のユーザーとその DBUnit に関する問題です。

于 2008-08-15T15:40:50.320 に答える
4

良い質問。

私も同様の問題を抱えており、抵抗が最も少ない道を歩みました(とにかく、私にとって)。

他の人が言及した他の解決策がたくさんあります。それらの多くは、他の人にとってより優れている/より純粋である/より適切です.

私はすでに Testdriven.NET/MbUnit を使用して C# をテストしていたので、各プロジェクトにテストを追加して、そのアプリで使用されるストアド プロシージャを呼び出すだけでした。

分かってる。これはひどいように聞こえますが、私が必要としているのは、いくつかのテストを開始して、そこから先に進むことです。このアプローチは、カバレッジが低いにもかかわらず、それらを呼び出すコードをテストしていると同時に、いくつかのストアド プロシージャをテストしていることを意味します。これにはいくつかの論理があります。

于 2008-10-02T11:40:52.273 に答える
3

元の投稿者とまったく同じ状況です。それは、パフォーマンスとテスト容易性に帰着します。私の偏見は、テスト可能性 (機能させる、正しくする、高速にする) を重視しており、ビジネス ロジックをデータベースから除外することを示唆しています。データベースには、Java などの言語に見られるテスト フレームワーク、コード ファクタリング構造、コード分析およびナビゲーション ツールが欠けているだけでなく、高度にファクタリングされたデータベース コードも低速です (高度にファクタリングされた Java コードはそうではありません)。

ただし、データベース セット処理の威力は認識しています。適切に使用すると、SQL は非常に小さなコードで非常に強力な機能を実行できます。したがって、ユニット・テストのためにできる限りのことを行いますが、データベース内にいくつかのセット・ベースのロジックが存在することに問題はありません。

これに関連して、非常に長い手続き型のデータベース コードは、他の何かの兆候であることが多いようです。そのようなコードは、パフォーマンスに影響を与えることなく、テスト可能なコードに変換できると思います。理論的には、このようなコードは、大量のデータを定期的に処理するバッチ プロセスを表すことが多いというものです。これらのバッチ プロセスが、入力データが変更されるたびに実行されるリアルタイム ビジネス ロジックの小さなチャンクに変換される場合、このロジックは、パフォーマンスに影響を与えることなく (テスト可能な場所である) 中間層で実行できます (作業はリアルタイムで小さなチャンクで行われます)。副次的な効果として、これにより、バッチ プロセスのエラー処理の長いフィードバック ループも解消されます。もちろん、このアプローチはすべての場合にうまくいくわけではありませんが、うまくいく場合もあります。また、システムにこのようなテスト不可能なバッチ処理データベース コードが大量にある場合、救済への道のりは長く困難なものになる可能性があります。YMMV。

于 2008-10-02T06:08:33.443 に答える
2

しかし、あなたは実際にはパフォーマンスにもっと関心を持っているように感じます。これは、実際には単体テストの領域ではありません。単体テストはかなりアトミックであると想定されており、パフォーマンスではなく動作をチェックすることを目的としています。その場合、ほぼ確実に、クエリ プランをチェックするためにプロダクション クラスのロードが必要になります。

ここには、パフォーマンスと、ストアド プロシージャの実際のロジックという、まったく異なる 2 つのテスト領域があると思います。

過去にデータベースのパフォーマンスをテストした例を挙げましたが、ありがたいことに、パフォーマンスが十分に良好なポイントに到達しました。

データベース内のすべてのビジネス ロジックが悪い状況であることには完全に同意しますが、これは開発者のほとんどが入社する前から受け継いできたものです。

ただし、現在、新しい機能に Web サービス モデルを採用しており、ストアド プロシージャを可能な限り回避し、ロジックを C# コードに保持し、データベースで SQLCommand を実行しています (ただし、linq は現在では推奨される方法)。既存の SP はまだ使用されているため、遡及的に単体テストを行うことを考えていました。

于 2008-08-15T16:53:22.110 に答える
2

Visual Studio for Database Professionals を試すこともできます。主に変更管理に関するものですが、テスト データと単体テストを生成するためのツールもあります。

それはかなり高価です。

于 2008-08-18T09:41:19.873 に答える
1

DataFreshを使用して各テスト間の変更をロールバックすると、sproc のテストは比較的簡単になります。

まだ不足しているのは、コード カバレッジ ツールです。

于 2008-08-15T16:41:34.100 に答える
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....
于 2009-04-29T03:32:32.710 に答える
0

ストアド プロシージャからロジックを削除し、それを linq クエリとして再実装する場合にのみ、LINQ はこれを簡素化します。これは間違いなく、はるかに堅牢でテストしやすいものです。ただし、要件によってこれが妨げられるようです。

TL;DR: あなたのデザインには問題があります。

于 2008-08-15T15:44:00.127 に答える
0

SP を呼び出す C# コードの単体テストを行います。
クリーンなテスト データベースを作成するビルド スクリプトがあります。
そして、より大きなものは、テスト フィクスチャ中に取り付けたり取り外したりします。
これらのテストには数時間かかる場合がありますが、それだけの価値があると思います。

于 2008-08-15T15:51:21.313 に答える
0

コードをリファクタリングする 1 つのオプション (醜いハックを認めます) は、CPP (C プリプロセッサ) M4 (試したことがない) などを介してコードを生成することです。私はまさにそれを行っているプロジェクトを持っており、実際にはほとんど実行可能です。

有効であると私が考える唯一のケースは、1) KLOC+ ストアド プロシージャの代替として、および 2) これが私のケースであり、プロジェクトのポイントは、テクノロジをどこまで (非常識に) プッシュできるかを確認することです。

于 2008-08-15T17:09:23.060 に答える
0

あらゆる種類のデータ関連プログラミングの単体テストの問題は、最初に信頼できる一連のテスト データを用意する必要があることです。ストアド プロシージャの複雑さとその機能にも大きく依存します。多くのテーブルを変更する非常に複雑な手順の単体テストを自動化するのは非常に困難です。

他の投稿者の中には、手動テストを自動化する簡単な方法や、SQL Server で使用できるツールについて言及している人もいます。Oracle 側では、PL/SQL の第一人者である Steven Feuerstein が、utPLSQL と呼ばれる PL/SQL ストアド プロシージャ用の無料のユニット テスト ツールに取り組みました。

しかし、彼はその努力をやめ、Quest の Code Tester for PL/SQL を商用化しました。Quest は、無料でダウンロードできる試用版を提供しています。私はそれを試してみる寸前です。私の理解では、テストフレームワークをセットアップする際のオーバーヘッドを処理して、テスト自体だけに集中できるようにするのが得意であり、テストを保持して回帰テストで再利用できるようにすることは、大きな利点の 1 つです。テスト駆動開発。さらに、出力変数をチェックするだけでなく、データの変更を検証するための準備も整っているはずですが、私自身も詳しく調べる必要があります。この情報は、Oracle ユーザーにとって価値があると思いました。

于 2008-08-18T00:07:13.277 に答える
0

ああ少年。sprocs は (自動化された) 単体テストには向いていません。私は、t-sql バッチ ファイルにテストを記述し、print ステートメントの出力と結果を手作業でチェックすることによって、複雑な sproc を "単体テスト" します。

于 2008-08-15T19:35:27.680 に答える