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

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

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

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

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

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

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

Ответов (16)

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

Я предполагаю, что вам нужно модульное тестирование в MSSQL. Если посмотреть на DBUnit, то есть некоторые ограничения в его поддержке MSSQL. Например, он не поддерживает NVarChar. Вот некоторые реальные пользователи и их проблемы с DBUnit.

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

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

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

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

Что-то подобное.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/*

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