문제

우리는 C#/C++ 코드에 대해 작성한 단위 테스트가 실제로 성과를 거두었다는 것을 발견했습니다.그러나 저장 프로시저에는 여전히 수천 줄의 비즈니스 논리가 있으며, 이는 우리 제품이 많은 수의 사용자에게 출시될 때만 분노하게 테스트됩니다.

상황을 더욱 악화시키는 것은 SP 간에 임시 테이블을 전달할 때 성능 저하로 인해 이러한 저장 프로시저 중 일부가 매우 길어진다는 것입니다.이로 인해 코드를 더 단순하게 만들기 위한 리팩토링이 불가능해졌습니다.

우리는 일부 주요 저장 프로시저(주로 성능 테스트)를 중심으로 단위 테스트를 구축하려고 여러 번 시도했지만 이러한 테스트를 위한 테스트 데이터를 설정하는 것이 정말 어렵다는 사실을 발견했습니다.예를 들어 테스트 데이터베이스를 복사하게 됩니다.이 외에도 테스트는 변경 사항에 매우 민감하며 저장된 프로세스에 대한 가장 작은 변경 사항도 발생합니다.또는 테이블에는 테스트에 많은 양의 변경이 필요합니다.따라서 이러한 데이터베이스 테스트가 간헐적으로 실패하여 많은 빌드가 중단된 후 우리는 이를 빌드 프로세스에서 제외해야 했습니다.

그래서 내 질문의 주요 부분은 다음과 같습니다.저장 프로시저에 대한 단위 테스트를 성공적으로 작성한 사람이 있습니까?

내 질문의 두 번째 부분은 linq를 사용하면 단위 테스트가 더 쉬울지/더 쉬울지 여부입니다.

나는 테스트 데이터 테이블을 설정하는 대신 단순히 테스트 개체 모음을 만들고 "linq to object" 상황에서 linq 코드를 테스트할 수 있다고 생각했습니다.(나는 linq를 처음 접했기 때문에 이것이 전혀 작동할지 모르겠습니다)

도움이 되었습니까?

해결책

나는 얼마 전에 이와 같은 문제를 겪었고 연결 및 트랜잭션을 주입할 수 있는 데이터 액세스를 위한 간단한 추상 기본 클래스를 생성한 경우 sprocs를 단위 테스트하여 SQL에서 작업을 수행했는지 확인할 수 있다는 것을 발견했습니다. 그들에게 요청한 다음 롤백하여 테스트 데이터가 db에 남지 않도록 했습니다.

이것은 일반적인 "테스트 DB를 설정하기 위해 스크립트를 실행한 다음 테스트 실행 후 정크/테스트 데이터를 정리하는 것"보다 더 좋게 느껴졌습니다.또한 이 테스트는 "이 테스트를 실행하기 전에 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 주기를 테스트하여 모든 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.

그러나 나는 당신이 실제로 단위 테스트의 영역이 아닌 성능에 더 관심을 갖고 있다는 인상을 받았습니다.단위 테스트는 상당히 원자적이어야 하며 성능보다는 동작을 확인하기 위한 것입니다.그리고 이 경우 쿼리 계획을 확인하려면 프로덕션급 로드가 필요할 것이 거의 확실합니다.

내 생각에는 여기에는 매우 뚜렷한 두 가지 테스트 영역이 있습니다.성능 및 저장 프로시저의 실제 논리.

예전에 DB 성능을 테스트한 예를 들었는데, 다행히도 충분히 성능이 좋은 수준에 이르렀습니다.

나는 데이터베이스의 모든 비즈니스 로직이 나쁜 상황이라는 점에 전적으로 동의합니다. 그러나 이는 대부분의 개발자가 회사에 합류하기 전에 우리가 물려받은 것입니다.

그러나 우리는 이제 새로운 기능을 위해 웹 서비스 모델을 채택하고 있으며 가능한 한 저장 프로시저를 피하고 C# 코드의 논리를 유지하고 데이터베이스에서 SQLCommands를 실행하려고 노력해 왔습니다. 선호하는 방법).기존 SP를 여전히 일부 사용하고 있기 때문에 소급하여 단위 테스트를 고려하고 있었습니다.

당신은 또한 시도할 수 있습니다 데이터베이스 전문가를 위한 Visual Studio.주로 변경 관리에 관한 것이지만 테스트 데이터 및 단위 테스트 생성을 위한 도구도 있습니다.

꽤 비싼 편인데 말이죠.

우리는 사용 데이터프레시 각 테스트 간의 변경 사항을 롤백하려면 sprocs 테스트가 비교적 쉽습니다.

아직 부족한 것은 코드 적용 도구입니다.

나는 가난한 사람의 단위 테스트를 수행합니다.제가 게으른 경우 테스트는 잠재적으로 문제가 있는 매개변수 값이 포함된 몇 가지 유효한 호출일 뿐입니다.

/*

--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) 프로젝트의 요점이 기술을 얼마나 멀리 (미칠 정도로) 추진할 수 있는지 확인하는 것입니다.

오 소년.sprocs는 (자동화된) 단위 테스트에 적합하지 않습니다.나는 t-sql 배치 파일에 테스트를 작성하고 print 문의 출력과 결과를 직접 확인하여 복잡한 sproc을 일종의 "단위 테스트"합니다.

모든 종류의 데이터 관련 프로그래밍 단위 테스트의 문제점은 시작하려면 신뢰할 수 있는 테스트 데이터 세트가 있어야 한다는 것입니다.또한 저장된 프로세스의 복잡성과 그 기능에 따라 많은 것이 달라집니다.많은 테이블을 수정하는 매우 복잡한 절차에 대한 단위 테스트를 자동화하는 것은 매우 어렵습니다.

다른 포스터 중 일부에서는 수동 테스트를 자동화하는 몇 가지 간단한 방법과 SQL Server에서 사용할 수 있는 몇 가지 도구를 언급했습니다.Oracle 측에서는 PL/SQL 전문가인 Steven Feuerstein이 utPLSQL이라는 PL/SQL 저장 프로시저용 무료 단위 테스트 도구를 개발했습니다.

그러나 그는 그러한 노력을 중단하고 Quest의 PL/SQL용 Code Tester를 상용화했습니다.Quest는 무료로 다운로드할 수 있는 평가판을 제공합니다.나는 그것을 시험해 보기 직전이다.테스트 자체에만 집중할 수 있도록 테스트 프레임워크를 설정하는 데 드는 오버헤드를 잘 처리하고, 회귀 테스트에서 재사용할 수 있도록 테스트를 유지한다는 것이 제가 이해한 바입니다. 테스트 주도 개발.또한 단순히 출력 변수를 확인하는 것 이상의 기능을 갖고 있으며 데이터 변경 사항을 검증하는 기능도 갖추고 있지만 아직은 자세히 살펴봐야 합니다.저는 이 정보가 Oracle 사용자에게 가치가 있을 것이라고 생각했습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top