SQL ストアド プロシージャの単体テストに成功した人はいますか?

StackOverflow https://stackoverflow.com/questions/12374

  •  08-06-2019
  •  | 
  •  

質問

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

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

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

したがって、私の質問の主な部分は次のとおりです。ストアド プロシージャの単体テストをうまく書いた人はいるでしょうか?

私の質問の 2 番目の部分は、単体テストは linq を使用すると簡単になるか、または簡単になるかどうかです。

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

役に立ちましたか?

解決

私も少し前にこれと同じ問題に遭遇しましたが、接続とトランザクションを挿入できるデータ アクセス用の単純な抽象基本クラスを作成すれば、sproc の単体テストを行って、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 サイクル全体をテストして、すべての sproc が適切に機能すること、および ado.net コードが左右のマッピングを正しく行うことを確認する方法を示す、テスト基本クラスを使用した簡単なテストです。

これが上記のデータ アクセス サンプルで使用されている "spGetProducts" sproc をテストしていないことはわかっていますが、sproc の単体テストに対するこのアプローチの背後にある威力がわかるはずです。

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

これが長い例であることは承知していますが、データ アクセス作業用に再利用可能なクラスを用意し、テスト用に別の再利用可能なクラスを用意することで、セットアップ/破棄作業を何度も行う必要がなくなりました ;)

他のヒント

やってみました DBユニット?これは、C# コードを実行する必要なく、データベースだけを単体テストするように設計されています。

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

私の皮肉な世界では、ストアド プロシージャは、ビジネス処理をデータベースに移行するよう説得する RDBMS の世界の長年にわたる試みの一部であり、サーバー ライセンスのコストがプロセッサ数などに関連する傾向があることを考慮すると、これは当然のことです。データベース内で実行するものが増えれば増えるほど、より多くの利益が得られます。

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

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

そんな感じ。

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

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

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

許容可能な時間内に実行するために、データ モデルへの変更を含めてクエリを完全に再設計する必要が生じることがよくあります。ストアド プロシージャでは、非常に優れたカプセル化が提供されるため、変更が呼び出し元に対して透過的に行われることが保証されます。

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

良い質問。

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

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

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

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

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

ただし、データベース セット処理の威力は認識しています。SQL を適切に使用すると、非常に少ないコードで信じられないほど強力な機能を実行できます。したがって、単体テストにできることはすべて行うつもりですが、データベース内にセットベースのロジックが存在していても問題ありません。

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

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

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

以前に db のパフォーマンスをテストする例を挙げましたが、ありがたいことに、パフォーマンスが十分に優れている点に到達しました。

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

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

試してみることもできます データベースプロフェッショナルのための Visual Studio. 。主に変更管理に関するものですが、テスト データや単体テストを生成するためのツールもあります。

かなり高価ですけどね。

を使用しております データフレッシュ 各テスト間の変更をロールバックすれば、sproc のテストは比較的簡単になります。

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

私は貧乏人の単体テストを行っています。私が怠け者であれば、テストは潜在的に問題のあるパラメーター値を含む有効な呼び出しをいくつか実行するだけです。

/*

--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 クエリとして再実装する場合にのみこれを簡素化します。これは間違いなく、はるかに堅牢でテストが簡単です。ただし、要件によってはこれが不可能になるようです。

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

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

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

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

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

あらゆる種類のデータ関連プログラミングの単体テストの問題は、まず信頼できるテスト データのセットが必要であることです。また、ストアド プロシージャの複雑さとその動作によっても大きく異なります。多くのテーブルを変更する非常に複雑な手順の単体テストを自動化することは非常に困難です。

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

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

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top