Кто-нибудь имел успех в модульном тестировании хранимых процедур SQL?

Weve обнаружил, что модульные тесты, написанные для нашего кода на С#/С++, действительно окупились. Но у нас все еще есть тысячи линий бизнес-логики в хранимых процедурах, которые только действительно проходят проверку в гневе, когда наш продукт выставляется большому числу пользователей.

Что еще хуже, так это то, что некоторые из этих хранимых процедур заканчиваются очень длинными из-за высокой производительности при передаче временных таблиц между SP. Это помешало нам сделать рефакторинг, чтобы сделать код более простым.

Мы предприняли несколько попыток построить единичные тесты вокруг некоторых наших хранимых процедур ключа (в первую очередь, тестирование производительности), но обнаружили, что настройка тестовых данных для этих тестов очень сложная. Например, мы копируем тестовые базы данных. В дополнение к этому, тесты в конечном итоге очень чувствительны к изменениям и даже наименьшее изменение хранимой процедуры. или таблица требует большого количества изменений в тестах. Таким образом, после того, как многие из них нарушают работу этих тестов базы данных с перерывами, нам просто пришлось вытащить их из процесса сборки.

Итак, основная часть моих вопросов: кто-нибудь когда-либо успешно записывал модульные тесты для своих хранимых процедур?

Вторая часть моих вопросов заключается в том, будет ли модульное тестирование легче/проще с linq?

Я думал, что вместо того, чтобы настраивать таблицы тестовых данных, вы могли бы просто создать коллекцию тестовых объектов и протестировать свой код linq в ситуации "linq to objects"? (Я совершенно новый для linq, поэтому не знаю, будет ли это вообще работать)

Ответ 1

Я снова столкнулся с этой проблемой и обнаружил, что если бы я создал простой абстрактный базовый класс для доступа к данным, который позволил мне ввести соединение и транзакцию, я мог бы unit test мои sprocs посмотреть, выполнили ли они работу в SQL, который я попросил сделать, а затем откат, чтобы ни одна из тестовых данных не осталась в db.

Это стало лучше, чем обычная "запустить script для установки моего тестового 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

И теперь в вашем unit test вы также можете наследовать из очень простого базового класса, который выполняет вашу настройку/откат, или сохраняйте это на основе unit test

ниже - простой базовый класс тестирования, который я использовал

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 делает левый правый правильное отображение

Я знаю, что это не проверяет sproc spGetProducts, используемый в приведенном выше образце доступа к данным, но вы должны увидеть, что этот подход подходит для тестирования модулей 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

Я знаю, что это длинный пример, но он помог получить класс повторного использования для работы с доступом к данным и еще один класс повторного использования для моего тестирования, поэтому мне не нужно было снова и снова запускать работу по установке/разрыву;)

Ответ 2

Вы пробовали DBUnit? Он предназначен для unit test вашей базы данных и просто вашей базы данных, без необходимости проходить код С#.

Ответ 3

Если вы думаете о том, какой код, который модульное тестирование имеет тенденцию поощрять: небольшие высокосвязные и низко связанные процедуры, вы должны в значительной степени уметь видеть, где, по крайней мере, часть проблемы может быть.

В моем циничном мире хранимые процедуры являются частью долговременной попытки РСУБД убедить вас перевести вашу бизнес-обработку в базу данных, что имеет смысл, если учесть, что затраты на серверные лицензии, как правило, связаны с такими вещами, как процессор сосчитать. Чем больше вещей вы используете в своей базе данных, тем больше они делают от вас.

Но у меня создается впечатление, что вы больше заинтересованы в производительности, что на самом деле не является гарантией модульного тестирования. Модульные тесты должны быть достаточно атомными и предназначены для проверки поведения, а не производительности. И в этом случае вам почти наверняка понадобится загрузка производственного класса, чтобы проверить планы запросов.

Я думаю, вам нужен другой класс тестовой среды. Я бы предложил копию производства как самую простую, предполагая, что безопасность не является проблемой. Затем для каждого выпуска кандидата вы начинаете с предыдущей версии, переносите с помощью процедур выпуска (что даст хорошее тестирование в качестве побочного эффекта) и запустит тайминги.

Что-то вроде этого.

Ответ 4

Ключом к тестированию хранимых процедур является запись script, который заполняет пустую базу данных данными, которые планируются заранее, чтобы привести к последовательному поведению при вызове хранимых процедур.

Я должен поместить свой голос для того, чтобы в значительной степени поддержать хранимые процедуры и разместить вашу бизнес-логику, где я (и большинство администраторов баз данных) считают, что она принадлежит, в базе данных.

Я знаю, что мы, как разработчики программного обеспечения, хотим красиво реорганизованный код, написанный на нашем любимом языке, содержать всю нашу важную логику, но реалии производительности в системах с большими объемами и критический характер целостности данных требуют от нас сделайте некоторые компромиссы. Код Sql может быть уродливым, повторяющимся и трудным для тестирования, но я не могу себе представить сложность настройки базы данных без полного контроля над запросами.

Я часто вынужден полностью перепроектировать запросы, включить изменения в модель данных, чтобы заставить вещи работать в течение приемлемого времени. С хранимыми процедурами я могу заверить, что изменения будут прозрачными для вызывающего, поскольку хранимая процедура обеспечивает такую ​​отличную инкапсуляцию.

Ответ 6

Хороший вопрос.

У меня схожие проблемы, и я пошел по пути наименьшего сопротивления (для меня, во всяком случае).

Есть множество других решений, о которых говорили другие. Многие из них лучше/более чисты/более подходят для других.

Я уже использовал Testdriven.NET/MbUnit для тестирования моего С#, поэтому я просто добавил тесты для каждого проекта, чтобы вызвать хранимые процедуры, используемые этим приложением.

Я знаю, я знаю. Это звучит ужасно, но мне нужно, чтобы свалиться на землю с некоторыми испытаниями и уйти оттуда. Этот подход означает, что хотя мой охват низкий, я тестирую некоторые хранимые процедуры одновременно с проверкой кода, который будет вызывать их. Для этого существует некоторая логика.

Ответ 7

Я в той же ситуации, что и оригинальный плакат. Это сводится к производительности и тестируемости. Мое предвзятое отношение к тестируемости (заставить его работать, делать все правильно, быстро), что предполагает сохранение бизнес-логики из базы данных. Базы данных не только не имеют инфраструктур тестирования, конструкций факторинга кода, а также инструментов анализа кода и навигации, найденных на таких языках, как Java, но очень часто используемый код базы данных также медленный (где высокофакторный код Java отсутствует).

Однако, я действительно признаю мощь обработки набора базы данных. При правильном использовании SQL может сделать невероятно мощный материал с очень маленьким кодом. Итак, я в порядке с некоторой логикой, основанной на множестве, живущей в базе данных, хотя я все равно сделаю все возможное, чтобы unit test это.

В соответствующей заметке кажется, что очень длинный и процедурный код базы данных часто является симптомом чего-то еще, и я думаю, что такой код можно преобразовать в тестируемый код, не подвергая себя хиту производительности. Теория состоит в том, что такой код часто представляет пакетные процессы, которые периодически обрабатывают большие объемы данных. Если эти пакетные процессы должны были быть преобразованы в более мелкие куски бизнес-логики реального времени, которые выполняются всякий раз, когда вводятся входные данные, эта логика может быть запущена на среднем уровне (где он может быть протестирован) без снижения производительности (поскольку работа выполняется в небольших кусках в режиме реального времени). В качестве побочного эффекта это также устраняет длинные обратные петли обработки ошибок пакетного процесса. Конечно, такой подход не будет работать во всех случаях, но он может работать в некоторых случаях. Кроме того, если в вашей системе есть тонны такого непроверяемого кода обработки пакетной обработки, дорога к спасению может быть длинной и трудной. YMMV.

Ответ 8

Но у меня создается впечатление, что вы больше заинтересованы в производительности, что на самом деле не является гарантией модульного тестирования. Модульные тесты должны быть достаточно атомными и предназначены для проверки поведения, а не производительности. И в этом случае вам почти наверняка понадобится загрузка производственного класса, чтобы проверить планы запросов.

Я думаю, что здесь есть две совершенно разные области тестирования: производительность и фактическая логика хранимых процедур.

Я привел пример тестирования производительности db в прошлом, и, к счастью, мы достигли точки, где производительность достаточно хороша.

Я полностью согласен с тем, что ситуация со всей бизнес-логикой в ​​базе данных плохая, но это то, что мы унаследовали от того, что большинство наших разработчиков присоединилось к компании.

Однако теперь мы используем модель веб-сервисов для наших новых функций, и мы стараемся как можно больше избегать хранимых процедур, сохраняя логику в коде С# и запуская SQLCommands в базе данных (хотя linq теперь будет предпочтительным методом). Существует еще некоторое использование существующих SP, поэтому я думал о ретроспективном тестировании модулей.

Ответ 9

Вы также можете попробовать Visual Studio для профессионалов баз данных. В основном это касается управления изменениями, но также имеет инструменты для генерации тестовых данных и модульных тестов.

Это довольно дорого.

Ответ 10

Мы используем DataFresh для отката изменений между каждым тестом, а тестирование sprocs относительно просто.

Что еще не хватает, это инструменты для покрытия кода.

Ответ 11

Я тестирую единицы бедных. Если я ленив, тест - это всего лишь пара действительных вызовов с потенциально проблемными значениями параметров.

/*

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

Ответ 12

LINQ упростит это, только если вы удалите логику из ваших хранимых процедур и переопределите ее как запросы linq. Это было бы гораздо более надежным и простым в тестировании. Тем не менее, это звучит так, как ваши требования будут препятствовать этому.

TL; DR: У вашего дизайна есть проблемы.

Ответ 13

Мы unit test код С#, который вызывает SP.
У нас есть скрипты сборки, создающие чистые тестовые базы данных.
И более крупные мы прикрепляем и отсоединяем во время тестирования. Эти тесты могут занять несколько часов, но я думаю, что это того стоит.

Ответ 14

Один из вариантов повторного кодирования кода (я признаю уродливый взлом) - это сгенерировать его через CPP (препроцессор C) M4 (никогда не пробовал) или тому подобное. У меня есть проект, который делает именно это, и он на самом деле в основном работоспособен.

Единственный случай, который, по моему мнению, может быть действительным, - это 1) в качестве альтернативы хранимым процедурам KLOC + и 2), и это мои случаи, когда точка проекта должна видеть, насколько далеко (в безумном) вы можете нажать технологии.

Ответ 15

О, мальчик. sprocs не поддаются (автоматическому) модульному тестированию. Я сортирую "unit test" мои сложные sprocs, записывая тесты в пакетные файлы t-sql и вручную проверяя вывод операторов печати и результатов.

Ответ 16

Проблема с модульным тестированием любого типа программирования, связанного с данными, заключается в том, что для начала нужно иметь надежный набор тестовых данных. Многое также зависит от сложности хранимой процедуры и того, что она делает. Было бы очень сложно автоматизировать модульное тестирование для очень сложной процедуры, которая изменила многие таблицы.

Некоторые другие плакаты отметили несколько простых способов автоматизировать их вручную, а также некоторые инструменты, которые вы можете использовать с SQL Server. С точки зрения Oracle, гуру PL/SQL Стивен Фейерстейн работал над бесплатным модульным инструментом тестирования для хранимых процедур PL/SQL, называемых utPLSQL.

Тем не менее, он отбросил это усилие, а затем отправился в продажу с помощью тестера Quest Code для PL/SQL. Quest предлагает бесплатную загружаемую пробную версию. Я нахожусь на грани попыток; я понимаю, что хорошо справляться с накладными расходами при настройке структуры тестирования, чтобы вы могли сосредоточиться только на самих тестах, и он держит тесты, чтобы вы могли повторно использовать их при регрессионном тестировании, что является одним из больших преимуществ тест-привод развития. Кроме того, предполагается, что он будет полезен не только для проверки выходной переменной, но и для обеспечения проверки изменений данных, но мне все же придется поближе познакомиться. Я думал, что эта информация может быть полезной для пользователей Oracle.