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

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

  •  08-06-2019
  •  | 
  •  

Вопрос

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

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

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

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

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

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

Это было полезно?

Решение

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top