2008-08-15 2 views
37

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

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

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

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

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

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

ответ

11

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

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

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

0

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

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

6

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

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

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

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

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

0

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

1

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

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

2

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

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

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

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

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

0

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

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

6

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

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

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

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

+0

Вы троллировали с помощью своего отброса, не так ли? Используется ли бизнес-логика в перегруженном смысле? Итак, как бы вы могли тестировать свою логику, с интеграцией или более высокими испытаниями стека? У меня было гораздо больше шансов сохранить прокрутки просто и сделать логический подъем в коде. У меня были ужасные впечатления от смехотворно вложенных sprocs и функций, чтобы гарантировать, что вся бизнес-логика осталась в БД. Не для меня спасибо. – brumScouse 2018-01-16 07:54:47

0

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

0

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

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

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

2

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

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

3

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

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

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

4

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

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

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

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

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

1

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

/* 

--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.... 
Смежные вопросы