Alguém já teve algum sucesso nos testes de unidade do SQL procedimentos armazenados?

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

  •  08-06-2019
  •  | 
  •  

Pergunta

Nós descobrimos que a unidade de testes que tenho escrito para o nosso C#/C++ código de ter realmente valeu a pena.Mas ainda temos milhares de linhas de lógica de negócios em procedimentos armazenados, que realmente só fazer o teste no raiva quando o nosso produto é lançado para um grande número de usuários.

O que torna isto pior é que alguns desses procedimentos armazenados acabam sendo muito longo, devido a perda de desempenho quando passar tabelas temporárias entre SPs.Isso nos impediu de refatoração para tornar o código mais simples.

Nós temos feito várias tentativas de construção de testes de unidade em torno de alguns de nossos principais procedimentos armazenados (principalmente testar o desempenho), mas têm encontrado que a configuração de dados de teste para esses testes é realmente difícil.Por exemplo, acabamos de copiar cerca de bancos de dados de teste.Além disso, os testes acabam sendo muito sensível à mudança, e até mesmo a menor mudança para um procedimento armazenado.ou tabela requer uma grande quantidade de alterações para os testes.Então, depois de muitas compilações de ruptura devido a estes testes do banco de dados falhar intermitentemente, nós apenas tivemos para retirá-los do processo de compilação.

Assim, a principal parte das minhas perguntas é:alguém já gravados com êxito os testes de unidade para os seus procedimentos armazenados?

A segunda parte das minhas dúvidas é se o teste de unidade seria/é mais fácil com o linq?

Eu estava pensando que, em vez de ter de criar tabelas de dados de teste, você pode simplesmente criar uma coleção de objetos de teste e testar o seu código do linq em um "linq to objects" situação?(Sou uma pessoa totalmente nova para o linq então não sei se este seria mesmo o trabalho de todos)

Foi útil?

Solução

Eu tive esse mesmo problema há um tempo atrás e achei que se eu criasse uma simples classe base abstrata para o acesso a dados que permitiu-me injetar uma conexão e transação, eu poderia teste de unidade o meu sprocs para ver se eles fizeram o trabalho em SQL que eu pedi para fazer e, em seguida, a reversão assim, nenhum dos dados de teste é deixado no banco.

Este sentia-se melhor do que o de costume "executar um script para configurar o meu teste db, em seguida, após a execução dos testes de fazer uma limpeza de lixo/dados de teste".Isso também me senti mais perto de testes de unidade, porque estes testes podem ser executados sozinho sem ter uma grande quantidade de "tudo em db precisa ser 'tão' antes de executar estes testes".

Aqui é um trecho da classe base abstrata utilizada para acesso a dados

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

em seguida, você vai ver um exemplo de classe de acesso a dados usando o acima da base de dados para obter uma lista de produtos

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

E agora, em sua unidade de teste, você também pode herdar de uma forma muito simples de classe base que faz o programa de configuração / reversão de trabalho ou manter isso por um teste de unidade base

abaixo está o teste simples de classe base que eu usei

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

e, finalmente, - o abaixo é um teste simples de utilizar que o teste da base de dados de classe que mostra como testar todo o CRUD ciclo certifique-se de que todos os sprocs fazer o seu trabalho e que sua ado.net código faz a esquerda-direita mapeamento corretamente

Eu sei que este não testa os "spGetProducts" sproc usado em acima de acesso a dados de exemplo, mas você deve ver o poder por trás desta abordagem para o teste de unidade de 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

Eu sei que isto é uma longa exemplo, mas ele ajudou a ter uma classe reutilizável para o acesso a dados de trabalho, e ainda outra classe reutilizável para o meu teste, então eu não tive que fazer a instalação/subdivisão de trabalho, mais e mais vezes ;)

Outras dicas

Você já tentou O dbunit?Ele foi projetado para teste de unidade da sua base de dados, e apenas o seu banco de dados, sem a necessidade de passar através de seu código C#.

Se você pensar sobre o tipo de código que o teste de unidade tende a promover:pequeno altamente coesa e humilde -, juntamente rotinas, em seguida, você deve muito ser capaz de ver onde pelo menos uma parte do problema pode ser.

Na minha cínica do mundo, procedimentos armazenados são parte do RDBMS mundial de longa tentativa de persuadi-lo a mover seu negócio de processamento no banco de dados, o que faz sentido quando você considera que o servidor de licença custos tendem a estar relacionados a coisas como o número de processadores.Quanto mais coisas você executar dentro de seu banco de dados, quanto mais de você.

Mas eu tenho a impressão de que você realmente está mais preocupado com o desempenho, o que não é realmente o preservar de testes de unidade em tudo.Testes de unidade são suposto ser bastante atômica e destinam-se a verificar o comportamento ao invés de desempenho.E, nesse caso, você está quase certamente vai precisar produção-classe de cargas, a fim de verificar planos de consulta.

Eu acho que você precisa de uma classe diferente de um ambiente de testes.Eu sugiro uma cópia de produção como o mais simples, supondo que a segurança não é um problema.Em seguida, para cada candidato lançamento, inicia com a versão anterior, migrar com seus procedimentos de liberação (que lhe dará os que um bom teste como um efeito colateral) e executar seus intervalos.

Algo assim.

A chave para teste de procedimentos armazenados é escrever um script que preenche um banco de dados em branco com os dados que é planejado com antecedência, para resultar em um comportamento consistente quando os procedimentos armazenados são chamados.

Eu tenho que colocar o meu voto para favorecendo fortemente procedimentos armazenados e colocando a sua lógica de negócios, onde eu (e a maioria dos DBAs) acho que ele pertence, no banco de dados.

Eu sei que nós, como engenheiros de software que deseja lindamente código reformulado, escrita em nosso idioma favorito, para conter todos os nossos importantes lógica, mas a realidade de desempenho em alto volume sistemas, e a natureza crítica de integridade de dados, a exigir-nos a fazer alguns compromissos.Código Sql pode ser feio, repetitivas e difíceis de testar, mas eu não consigo imaginar a dificuldade do ajuste de um banco de dados sem ter controle completo sobre o design da consulta.

Muitas vezes sou forçado a redesenhar completamente consultas, para incluir alterações no modelo de dados, para fazer as coisas para ser executado em uma quantidade de tempo aceitável.Com procedimentos armazenados, posso garantir que as mudanças sejam transparentes para o chamador, como um procedimento armazenado oferece excelentes encapsulamento.

Eu estou supondo que você deseja testes de unidade em MSSQL.Olhando para o dbunit existem algumas limitações no suporte a MSSQL.Ele não suporta o tipo de dados NVarChar, por exemplo. Aqui estão alguns usuários reais e seus problemas com o dbunit.

Boa questão.

Eu tenho problemas semelhantes, e tomei o caminho de menor resistência (para mim, pelo menos).

Há um monte de outras soluções, o que outros já mencionada.Muitos deles são melhores e mais puro / mais apropriada para outros.

Eu já estava usando Testdriven.NET/MbUnit para testar o meu C#, então eu simplesmente adicionado testes para cada um projeto para chamar procedimentos armazenados usados pelo aplicativo.

Eu sei, eu sei.Isso soa terrível, mas o que eu preciso é sair do chão com alguns o teste, e de lá ir.Esta abordagem significa que, apesar de a minha cobertura é baixa eu estou testando alguns procedimentos armazenados ao mesmo tempo que eu estou testando o código que irá chamar-lhes.Há alguma lógica para isso.

Eu estou na mesma situação exata como o poster original.Ele vem para baixo para o desempenho versus a capacidade de teste.Meu preconceito é com a testabilidade (fazer isso funcionar, faça direito, faça-lo rápido), o que sugere manter a lógica de negócios do banco de dados.Bancos de dados não só falta as estruturas de teste, o código de factoring construções, e de análise de código e ferramentas de navegação encontrada em linguagens como Java, mas altamente consideradas código de banco de dados também é lento (onde altamente consideradas código Java não é).

No entanto, eu não reconhecem o poder do conjunto de banco de dados de processamento.Quando usado adequadamente, o SQL pode fazer alguns incrivelmente poderoso, com muito pouco código.Então, eu estou ok com algumas definir a lógica baseada em viver no banco de dados, mesmo que eu ainda vou fazer tudo que posso para teste de unidade-lo.

Em uma nota relacionada, parece que a muito tempo e processual, o código de banco de dados é muitas vezes um sintoma de algo mais, e eu acho que esse código pode ser convertido para código testável, sem incorrer em um impacto no desempenho.A teoria é de que esse código representa, muitas vezes os processos em lote que periodicamente processar grandes quantidades de dados.Se estes processos em lotes eram para ser convertido em pequenos pedaços de negócios em tempo real, a lógica que é executado sempre que a entrada de dados for alterado, essa lógica pode ser executado na camada intermediária (onde ele pode ser testado) sem tomar um impacto no desempenho (já que o trabalho é feito em pequenos pedaços em tempo real).Como um efeito colateral, isso também elimina o tempo de feedback loops de lote processo de tratamento de erro.É claro que esta abordagem não funciona em todos os casos, mas pode trabalhar em alguns.Além disso, se há toneladas de tal não testáveis processamento em lote código do banco de dados em seu sistema, o caminho para a salvação pode ser longo e árduo.YMMV.

Mas eu tenho a impressão de que você realmente está mais preocupado com o desempenho, o que não é realmente o preservar de testes de unidade em tudo.Testes de unidade são suposto ser bastante atômica e destinam-se a verificar o comportamento ao invés de desempenho.E, nesse caso, você está quase certamente vai precisar produção-classe de cargas, a fim de verificar planos de consulta.

Eu acho que existem dois bastante distintos testes de áreas aqui:o desempenho e a lógica real de procedimentos armazenados.

Eu dei o exemplo do teste, o db desempenho no passado e, felizmente, chegamos a um ponto em que o desempenho é bom o suficiente.

Eu concordo completamente que a situação com toda a lógica de negócio no banco de dados é um ruim, mas é algo que temos herdado de, antes de a maioria de nossos desenvolvedores juntou-se à empresa.

No entanto, agora estamos adotando o modelo de serviços web para nossos novos recursos, e estamos tentando evitar procedimentos armazenados tanto quanto possível, mantendo a lógica no código C# e disparando SQLCommands no banco de dados (embora linq passaria a ser o método preferido).Há ainda alguns existente SPs que foi por isso que eu estava pensando retrospectivamente os testes de unidade-los.

Você também pode tentar O Visual Studio para Profissionais de base de Dados.É principalmente sobre a mudança de gestão, mas também tem ferramentas para a geração de dados de teste e os testes de unidade.

Ele é muito caro, tho.

Nós usamos DataFresh a reversão de alterações entre cada teste, o teste sprocs é relativamente fácil.

O que ainda falta é a cobertura de código de ferramentas.

Eu homem pobre testes de unidade.Se eu sou preguiçoso, o teste é apenas um par de válido invocações potencialmente problemático valores de parâmetro.

/*

--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 vai simplificar isso somente se você remover a lógica de procedimentos armazenados e reimplementar como consultas linq.O que seria muito mais robusta e mais fácil de testar, definitivamente.No entanto, parece que o seu requisitos para impedir isso.

TL;DR:Seu design tem problemas.

Nós o teste de unidade de código C# que chama a SPs.
Temos que construir os scripts, criação de limpar bancos de dados de teste.
E maiores de anexar e desanexar durante o teste de fixação.
Estes testes pode demorar horas, mas eu acho que vale a pena.

Uma opção para re-fator de código (eu vou admitir um feio corte) seria gerar através CPP (o pré-processador C) M4 (nunca tentei) ou semelhantes.Eu tenho um projeto que está fazendo exatamente isso e ele é, na verdade, a maioria viável.

O único caso que eu acho que pode ser válido para 1) como uma alternativa para KLOC+ procedimentos armazenados e 2) e esta é a minha casos, quando o ponto de projeto é ver o quão longe (em insane) você pode empurrar uma tecnologia.

Oh, rapaz.sprocs não se prestam a (automático), o teste de unidade.Eu espécie de "teste de unidade" meu complexo sprocs escrevendo testes em t-sql em lote de arquivos de e mão de verificar a saída de instruções de impressão e os resultados.

O problema com a unidade de teste de qualquer tipo de dados-relacionados a programação é que você tem que ter um confiável conjunto de dados de teste para começar.Muito também depende da complexidade do procedimento e o que ele faz.Seria muito difícil para automatizar os testes de unidade para um complexo procedimento que modificou muitas tabelas.

Alguns dos outros cartazes ter notado algumas maneiras simples para automatizar manualmente a testá-los, e também algumas ferramentas que você pode usar com o SQL Server.No lado do Oracle, PL/SQL guru Steven Feuerstein trabalhou numa unidade de ferramenta de teste para PL/SQL procedimentos armazenados chamado utPLSQL.

No entanto, ele deixou que o esforço e, em seguida, passou comerciais com a Busca do Código Verificador para PL/SQL.Quest oferece para download gratuito na versão de avaliação.Eu estou à beira de tentá-lo para fora;o meu entendimento é de que ele é bom tomar cuidado com a sobrecarga na definição de um framework de testes para que você possa se concentrar em apenas testes, e mantém-se a testes para que você possa reutilizá-los no teste de regressão, um dos grandes benefícios de test-driven-development.Além disso, é suposto ser bom em mais do que apenas a verificação de uma variável de saída e não tem disposição para validar as alterações de dados, mas ainda tenho que dar uma olhada mais de perto de mim.Eu pensei que esta informação pode ser de valor para usuários Oracle.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top