Como testar a unidade de um objeto com consultas ao banco de dados

153

Ouvi dizer que o teste de unidade é "totalmente incrível", "muito legal" e "todo tipo de coisa boa", mas 70% ou mais dos meus arquivos envolvem acesso a bancos de dados (alguns lêem e outros escrevem) e não tenho certeza de como para escrever um teste de unidade para esses arquivos.

Estou usando PHP e Python, mas acho que é uma pergunta que se aplica à maioria das linguagens que usam acesso ao banco de dados.

Teifion
fonte

Respostas:

82

Eu sugeriria zombar de suas chamadas para o banco de dados. As zombarias são basicamente objetos que se parecem com o objeto no qual você está tentando chamar um método, no sentido de que eles têm as mesmas propriedades, métodos etc. disponíveis para o chamador. Mas, em vez de executar qualquer ação que eles estão programados para executar quando um método específico é chamado, ele ignora isso e retorna um resultado. Esse resultado geralmente é definido por você com antecedência.

Para configurar seus objetos para zombaria, você provavelmente precisará usar algum tipo de inversão do padrão de injeção de controle / dependência, como no pseudocódigo a seguir:

class Bar
{
    private FooDataProvider _dataProvider;

    public instantiate(FooDataProvider dataProvider) {
        _dataProvider = dataProvider;
    }

    public getAllFoos() {
        // instead of calling Foo.GetAll() here, we are introducing an extra layer of abstraction
        return _dataProvider.GetAllFoos();
    }
}

class FooDataProvider
{
    public Foo[] GetAllFoos() {
        return Foo.GetAll();
    }
}

Agora, no seu teste de unidade, você cria uma simulação do FooDataProvider, que permite chamar o método GetAllFoos sem precisar acessar o banco de dados.

class BarTests
{
    public TestGetAllFoos() {
        // here we set up our mock FooDataProvider
        mockRepository = MockingFramework.new()
        mockFooDataProvider = mockRepository.CreateMockOfType(FooDataProvider);

        // create a new array of Foo objects
        testFooArray = new Foo[] {Foo.new(), Foo.new(), Foo.new()}

        // the next statement will cause testFooArray to be returned every time we call FooDAtaProvider.GetAllFoos,
        // instead of calling to the database and returning whatever is in there
        // ExpectCallTo and Returns are methods provided by our imaginary mocking framework
        ExpectCallTo(mockFooDataProvider.GetAllFoos).Returns(testFooArray)

        // now begins our actual unit test
        testBar = new Bar(mockFooDataProvider)
        baz = testBar.GetAllFoos()

        // baz should now equal the testFooArray object we created earlier
        Assert.AreEqual(3, baz.length)
    }
}

Um cenário de zombaria comum, em poucas palavras. É claro que você provavelmente ainda desejará testar também suas chamadas de banco de dados reais, pelas quais precisará acessar o banco de dados.

Doug R
fonte
Eu sei que isso é antigo, mas que tal criar uma tabela duplicada para a que já está no banco de dados? Dessa forma, você pode confirmar que as chamadas ao banco de dados funcionam?
Bretterer 3/10/12
1
Eu tenho usado o PDO do PHP como meu acesso de nível mais baixo ao banco de dados, sobre o qual extraí uma interface. Em seguida, construí uma camada de banco de dados com reconhecimento de aplicativo. Essa é a camada que contém todas as consultas SQL brutas e outras informações. O restante do aplicativo interage com esse banco de dados de nível superior. Descobri que isso funciona muito bem para testes de unidade; Testo minhas páginas de aplicativos em como elas interagem com o banco de dados de aplicativos. Testo meu banco de dados de aplicativos em como ele interage com o DOP. Presumo que o DOP funcione sem erros. Código fonte: manx.codeplex.com
legalize
1
@ bretterer - Criar uma tabela duplicada é bom para testes de integração. Para testes de unidade, você usaria um objeto simulado que permitirá testar uma unidade de código, independentemente do banco de dados.
BornToCode
2
Qual é o valor de zombar de chamadas de banco de dados em seus testes de unidade? Não parece útil porque você pode alterar a implementação para retornar um resultado diferente, mas seu teste de unidade (incorretamente) passaria.
precisa saber é
2
@ bmay2 Você não está errado. Minha resposta original foi escrita há muito tempo (9 anos!), Quando muitas pessoas não estavam escrevendo seu código de maneira testável e quando as ferramentas de teste estavam em falta. Eu não recomendaria mais essa abordagem. Hoje, eu apenas configurava um banco de dados de teste e o preenchia com os dados necessários para o teste e / ou projetava meu código para poder testar o máximo de lógica possível sem um banco de dados.
Doug R
25

Idealmente, seus objetos devem ser persistentes e ignorantes. Por exemplo, você deve ter uma "camada de acesso a dados", para a qual solicitaria e que retornaria objetos. Dessa forma, você pode deixar essa parte fora dos testes de unidade ou testá-los isoladamente.

Se seus objetos estão fortemente acoplados à sua camada de dados, é difícil fazer o teste de unidade adequado. a primeira parte do teste de unidade é "unidade". Todas as unidades devem poder ser testadas isoladamente.

Nos meus projetos c #, eu uso o NHibernate com uma camada de dados completamente separada. Meus objetos vivem no modelo de domínio principal e são acessados ​​na minha camada de aplicativo. A camada de aplicativo conversa com a camada de dados e a camada de modelo de domínio.

A camada de aplicação também é chamada de "Camada de negócios".

Se você estiver usando PHP, crie um conjunto específico de classes para ONLY acesso a dados. Verifique se seus objetos não têm idéia de como eles são persistentes e conecte os dois nas classes de aplicativos.

Outra opção seria usar zombarias / stubs.

Sean Chambers
fonte
Eu sempre concordei com isso, mas, na prática, devido a prazos e "tudo bem, agora adicione apenas mais um recurso, até as 14h de hoje", essa é uma das coisas mais difíceis de alcançar. Esse tipo de coisa é o principal objetivo da refatoração, se meu chefe decidir que não pensou em 50 novos problemas emergentes que exigem lógica e tabelas de negócios totalmente novas.
Darren Ringer
3
Se seus objetos estão fortemente acoplados à sua camada de dados, é difícil fazer o teste de unidade adequado. a primeira parte do teste de unidade é "unidade". Todas as unidades devem poder ser testadas isoladamente. boa explicação
Amitābha
11

A maneira mais fácil de testar um objeto com acesso ao banco de dados é usando escopos de transação.

Por exemplo:

    [Test]
    [ExpectedException(typeof(NotFoundException))]
    public void DeleteAttendee() {

        using(TransactionScope scope = new TransactionScope()) {
            Attendee anAttendee = Attendee.Get(3);
            anAttendee.Delete();
            anAttendee.Save();

            //Try reloading. Instance should have been deleted.
            Attendee deletedAttendee = Attendee.Get(3);
        }
    }

Isso reverterá o estado do banco de dados, basicamente como uma reversão de transação, para que você possa executar o teste quantas vezes quiser, sem efeitos colaterais. Usamos essa abordagem com sucesso em grandes projetos. Nossa compilação demora um pouco para ser executada (15 minutos), mas não é horrível ter 1800 testes de unidade. Além disso, se o tempo de compilação for uma preocupação, você pode alterar o processo de compilação para ter várias compilações, uma para a criação de src, outra que é acionada posteriormente que lida com testes de unidade, análise de código, empacotamento, etc.

BELEZA.
fonte
1
+1 Economiza muito tempo ao testar as unidades de acesso a dados. Nota apenas que TS muitas vezes será necessário MSDTC que não pode ser desejável (dependendo se o seu aplicativo precisará MSDTC)
StuartLC
A pergunta original era sobre PHP, este exemplo parece ser C #. Os ambientes são muito diferentes.
legalize
2
O autor da pergunta afirmou que é uma pergunta geral aplicável a todos os idiomas que têm algo a ver com um banco de dados.
Vedran
9
e este queridos amigos, é chamado de testes de integração
AA.
10

Talvez eu possa lhe dar um gostinho da nossa experiência quando começamos a analisar os testes de unidade de nosso processo de camada intermediária, que incluíam uma tonelada de operações sql de "lógica de negócios".

Primeiro, criamos uma camada de abstração que nos permitia "encaixar" qualquer conexão de banco de dados razoável (no nosso caso, simplesmente suportávamos uma única conexão do tipo ODBC).

Quando isso aconteceu, pudemos fazer algo assim em nosso código (trabalhamos em C ++, mas tenho certeza de que você entendeu):

GetDatabase (). ExecuteSQL ("INSERIR EM FOO (blá, blá)")

Em tempo de execução normal, GetDatabase () retornaria um objeto que alimentava todo o nosso sql (incluindo consultas), via ODBC diretamente no banco de dados.

Começamos então a olhar para os bancos de dados na memória - o melhor parece ser o SQLite. ( http://www.sqlite.org/index.html ). É incrivelmente simples de configurar e usar, e nos permitiu subclassar e substituir GetDatabase () para encaminhar o sql para um banco de dados na memória que foi criado e destruído para cada teste realizado.

Ainda estamos nos estágios iniciais disso, mas parece bom até agora, no entanto, precisamos garantir a criação de quaisquer tabelas necessárias e preenchê-las com dados de teste - no entanto, reduzimos a carga de trabalho aqui criando um conjunto genérico de funções auxiliares que pode fazer muito disso tudo por nós.

No geral, ajudou imensamente em nosso processo TDD, pois fazer o que parece ser mudanças bastante inócuas para corrigir certos bugs pode ter efeitos bastante estranhos em outras áreas (difíceis de detectar) do seu sistema - devido à natureza do sql / bancos de dados.

Obviamente, nossas experiências se concentraram em um ambiente de desenvolvimento em C ++, no entanto, tenho certeza de que talvez você possa obter algo semelhante trabalhando em PHP / Python.

Espero que isto ajude.

Alan
fonte
9

Você deve simular o acesso ao banco de dados se desejar testar suas classes. Afinal, você não deseja testar o banco de dados em um teste de unidade. Isso seria um teste de integração.

Abstraia as chamadas ausentes e insira uma simulação que retorne os dados esperados. Se suas classes não fazem mais do que executar consultas, pode até não valer a pena testá-las, embora ...

Martin Klinke
fonte
6

O livro xUnit Test Patterns descreve algumas maneiras de lidar com o código de teste de unidade que atinge um banco de dados. Eu concordo com as outras pessoas que estão dizendo que você não quer fazer isso porque é lento, mas você precisa fazê-lo em algum momento, IMO. Zombar da conexão db para testar coisas de nível superior é uma boa idéia, mas confira este livro para obter sugestões sobre coisas que você pode fazer para interagir com o banco de dados real.

Chris Farmer
fonte
4

Opções que você tem:

  • Escreva um script que limpe o banco de dados antes de iniciar os testes de unidade, preencha o banco de dados com um conjunto de dados predefinido e execute os testes. Você também pode fazer isso antes de cada teste - será lento, mas menos propenso a erros.
  • Injete o banco de dados. (Exemplo em pseudo-Java, mas se aplica a todas as linguagens OO)

    banco de dados da classe {
     consulta pública do resultado (consulta da corda) {... db real aqui ...}
    }

    classe MockDatabase estende o banco de dados { consulta pública de resultado (consulta de cadeia) { retornar "resultado simulado"; } }

    classe ObjectThatUsesDB { public ObjectThatUsesDB (banco de dados db) { this.database = db; } }

    agora em produção, você usa o banco de dados normal e, para todos os testes, apenas injeta o banco de dados falso que pode criar ad hoc.

  • Não use o DB na maior parte do código (isso é uma prática ruim de qualquer maneira). Crie um objeto "banco de dados" que, em vez de retornar com resultados, retornará objetos normais (ou seja, retornará em Uservez de uma tupla {name: "marcin", password: "blah"}), escreva todos os seus testes com objetos reais construídos ad hoc e escreva um grande teste que depende de um banco de dados que garanta essa conversão funciona bem.

É claro que essas abordagens não são mutuamente exclusivas e você pode combiná-las e combiná-las conforme necessário.

Marcin
fonte
3

O teste unitário do acesso ao banco de dados é bastante fácil se o seu projeto tiver alta coesão e acoplamento solto por toda parte. Dessa forma, você pode testar apenas o que cada classe em particular faz sem ter que testar tudo de uma vez.

Por exemplo, se você testar a unidade da classe de interface do usuário, os testes que você escrever deverão tentar apenas verificar se a lógica dentro da interface do usuário funcionou conforme o esperado, não a lógica comercial ou a ação do banco de dados por trás dessa função.

Se você deseja testar o acesso real ao banco de dados, você acabará realizando mais um teste de integração, porque dependerá da pilha de rede e do servidor de banco de dados, mas poderá verificar se seu código SQL faz o que você solicitou. Faz.

O poder oculto do teste de unidade para mim é que ele me obriga a projetar meus aplicativos de uma maneira muito melhor do que eu faria sem eles. Isso é porque realmente me ajudou a romper com a mentalidade "essa função deve fazer tudo".

Desculpe, não tenho exemplos de código específicos para PHP / Python, mas se você quiser ver um exemplo do .NET, tenho um post que descreve uma técnica que eu costumava fazer no mesmo teste.

Toran Billups
fonte
2

Normalmente, tento interromper meus testes entre testar os objetos (e ORM, se houver) e testar o banco de dados. Testo o lado do objeto, zombando das chamadas de acesso a dados, enquanto testo o lado do banco de dados, testando as interações do objeto com o banco de dados que, na minha experiência, geralmente é bastante limitado.

Eu costumava ficar frustrado com a gravação de testes de unidade até começar a zombar da parte de acesso a dados, para não precisar criar um banco de dados de teste ou gerar dados de teste em tempo real. Ao ridicularizar os dados, você pode gerar tudo em tempo de execução e garantir que seus objetos funcionem corretamente com entradas conhecidas.

akmad
fonte
2

Eu nunca fiz isso em PHP e nunca usei Python, mas o que você quer fazer é zombar das chamadas para o banco de dados. Para fazer isso, você pode implementar alguma IoC, seja uma ferramenta de terceiros ou você mesmo a gerencia, então você pode implementar uma versão simulada do chamador do banco de dados, onde é onde você controlará o resultado dessa chamada falsa.

Uma forma simples de IoC pode ser executada apenas codificando para Interfaces. Isso requer algum tipo de orientação a objetos em seu código, para que não se aplique ao que você está fazendo (eu digo que, como tudo o que tenho a fazer é sua menção a PHP e Python)

Espero que seja útil, se nada mais, você tem alguns termos para pesquisar agora.

codeLes
fonte
2

Concordo com o primeiro acesso pós-banco de dados deve ser retirado em uma camada DAO que implementa uma interface. Em seguida, você pode testar sua lógica em relação à implementação de stub da camada DAO.

Chris Marasti-Georg
fonte
2

Você pode usar estruturas de simulação para abstrair o mecanismo de banco de dados. Eu não sei se PHP / Python tem algum, mas para linguagens digitadas (C #, Java etc.), existem muitas opções

Também depende de como você projetou o código de acesso ao banco de dados, porque alguns projetos são mais fáceis de realizar testes de unidade do que outros, como os posts anteriores mencionaram.

chakrit
fonte
2

Configurar dados de teste para testes de unidade pode ser um desafio.

Quando se trata de Java, se você usa APIs do Spring para teste de unidade, pode controlar as transações no nível da unidade. Em outras palavras, você pode executar testes de unidade que envolvem atualizações / inserções / exclusões de banco de dados e reverter as alterações. No final da execução, você deixa tudo no banco de dados como estava antes de iniciar a execução. Para mim, é o melhor possível.

Bino Manjasseril
fonte