У кого-нибудь был успех в модульном тестировании хранимых процедур SQL? - PullRequest
37 голосов
/ 15 августа 2008

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

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

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

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

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

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

Ответы [ 16 ]

12 голосов
/ 24 августа 2008

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

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

10 голосов
/ 15 августа 2008

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

6 голосов
/ 15 августа 2008

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

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

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

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

6 голосов
/ 15 августа 2008

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

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

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

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

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

4 голосов
/ 02 октября 2008

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

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

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

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

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

4 голосов
/ 15 августа 2008

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

3 голосов
/ 02 октября 2008

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

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

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

2 голосов
/ 18 августа 2008

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

Это довольно дорого, хотя

2 голосов
/ 15 августа 2008

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

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

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

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

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

1 голос
/ 29 апреля 2009

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

/*

--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....
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...