Qual é a melhor estratégia para testar aplicativos controlados por banco de dados de unidade?

346

Trabalho com muitos aplicativos da Web que são direcionados por bancos de dados de complexidade variada no back-end. Normalmente, há uma camada ORM separada da lógica de negócios e de apresentação. Isso torna o teste de unidade da lógica de negócios bastante simples; as coisas podem ser implementadas em módulos discretos e todos os dados necessários para o teste podem ser falsificados por meio de zombaria de objetos.

Mas testar o ORM e o próprio banco de dados sempre foi repleto de problemas e compromissos.

Ao longo dos anos, tentei algumas estratégias, nenhuma das quais me satisfez completamente.

  • Carregue um banco de dados de teste com dados conhecidos. Execute testes no ORM e confirme se os dados corretos retornam. A desvantagem aqui é que o banco de dados de teste precisa acompanhar as alterações de esquema no banco de dados do aplicativo e pode ficar fora de sincronia. Ele também depende de dados artificiais e não pode expor bugs que ocorrem devido a entradas estúpidas do usuário. Finalmente, se o banco de dados de teste for pequeno, não revelará ineficiências como um índice ausente. (OK, esse último não é realmente para o que o teste de unidade deve ser usado, mas não dói.)

  • Carregue uma cópia do banco de dados de produção e teste contra isso. O problema aqui é que você pode não ter idéia do que está no banco de dados de produção a qualquer momento; seus testes podem precisar ser reescritos se os dados mudarem com o tempo.

Algumas pessoas apontaram que essas duas estratégias dependem de dados específicos, e um teste de unidade deve testar apenas a funcionalidade. Para esse fim, vi sugestões:

  • Use um servidor de banco de dados simulado e verifique apenas se o ORM está enviando as consultas corretas em resposta a uma determinada chamada de método.

Quais estratégias você usou para testar aplicativos orientados a banco de dados, se houver? O que funcionou melhor para você?

friedo
fonte
Eu acho que você ainda deve ter índices de banco de dados em um ambiente de teste para casos como índices exclusivos.
dtc
Pesonally não me importo com essa pergunta aqui, mas se seguirmos as regras, essa questão não é para stackoverflow, mas sim para o site softwareengineering.stackexchange .
ITExpert 30/08/19

Respostas:

155

Na verdade, eu usei sua primeira abordagem com algum sucesso, mas de maneiras um pouco diferentes que resolveriam alguns dos seus problemas:

  1. Mantenha todo o esquema e scripts para criá-lo no controle de origem, para que qualquer pessoa possa criar o esquema atual do banco de dados após um check-out. Além disso, mantenha dados de amostra em arquivos de dados que são carregados por parte do processo de construção. Ao descobrir dados que causam erros, adicione-os aos dados de amostra para verificar se os erros não voltam a aparecer.

  2. Use um servidor de integração contínua para construir o esquema do banco de dados, carregar os dados de amostra e executar testes. É assim que mantemos nosso banco de dados de teste sincronizado (reconstruindo-o a cada execução de teste). Embora isso exija que o servidor de IC tenha acesso e propriedade de sua própria instância de banco de dados dedicada, digo que a criação do nosso esquema db 3 vezes ao dia ajudou a encontrar drasticamente erros que provavelmente não seriam encontrados antes da entrega (se não mais tarde) ) Não posso dizer que reconstruo o esquema antes de cada confirmação. Alguem? Com essa abordagem, você não precisará (bem, talvez devêssemos, mas não é grande coisa se alguém esquecer).

  3. Para o meu grupo, a entrada do usuário é feita no nível do aplicativo (não no banco de dados), portanto, isso é testado através de testes de unidade padrão.

Carregando a cópia do banco de dados de produção:
essa foi a abordagem usada no meu último trabalho. Foi uma enorme dor causada por alguns problemas:

  1. A cópia ficaria desatualizada da versão de produção
  2. As alterações seriam feitas no esquema da cópia e não seriam propagadas para os sistemas de produção. Nesse ponto, teríamos esquemas divergentes. Não tem graça.

Zombando do servidor de banco de dados:
Também fazemos isso no meu trabalho atual. Após cada confirmação, executamos testes de unidade contra o código do aplicativo que injetou acessadores de banco de dados simulados. Em seguida, três vezes ao dia, executamos a compilação completa do banco de dados descrita acima. Definitivamente, recomendo as duas abordagens.

Mark Roddy
fonte
37
O carregamento de uma cópia do banco de dados de produção também tem implicações de segurança e privacidade. Quando ficar grande, tirar uma cópia e colocá-la no seu ambiente de desenvolvimento pode ser um grande problema.
WW.
honestamente, isso é uma dor enorme. Sou novo no teste e também escrevi um orm que quero testar. Eu já usei o seu primeiro método, mas li que ele não faz a unidade de teste. Eu uso a funcionalidade específica do mecanismo de banco de dados e, portanto, zombar de um DAO será difícil. Eu acho que apenas uso meu método atual, pois ele funciona e outros o usam. Testes automatizados são bem-vindos. Obrigado.
Frostymarvelous
2
Eu gerencio dois projetos grandes e diferentes, em um deles essa abordagem foi perfeita, mas estamos enfrentando muitos problemas ao tentar implementar isso no outro projeto. Então eu acho que depende da facilidade com que o esquema pode ser recriado a cada vez para executar os testes. No momento, estou trabalhando para encontrar uma nova solução para esse último problema.
Cruzar
2
Nesse caso, definitivamente vale a pena usar uma ferramenta de versão de banco de dados como Roundhouse - algo que pode executar migrações. Isso pode ser executado em qualquer instância do banco de dados e deve garantir que os esquemas estejam atualizados. Além disso, quando os scripts de migração são gravados, os dados de teste devem ser gravados, mantendo as migrações e os dados sincronizados.
precisa saber é o seguinte
melhor aproveitamento macaco patching e zombando e operações de escrita Evitar
Nickpick
56

Estou sempre executando testes em um banco de dados na memória (HSQLDB ou Derby) por estes motivos:

  • Isso faz você pensar quais dados manter no banco de dados de teste e por quê. Apenas transportar seu banco de dados de produção para um sistema de teste se traduz em "Não faço ideia do que estou fazendo ou por que, e se algo quebrar, não fui eu !!" ;)
  • Isso garante que o banco de dados possa ser recriado com pouco esforço em um novo local (por exemplo, quando precisamos replicar um bug da produção)
  • Ajuda enormemente com a qualidade dos arquivos DDL.

O banco de dados na memória é carregado com dados atualizados após o início dos testes e após a maioria dos testes, invoco o ROLLBACK para mantê-lo estável. SEMPRE mantenha os dados no banco de dados de teste estável! Se os dados mudarem o tempo todo, você não poderá testar.

Os dados são carregados do SQL, um banco de dados modelo ou um dump / backup. Eu prefiro despejos se eles estiverem em um formato legível, porque eu posso colocá-los no VCS. Se isso não funcionar, eu uso um arquivo CSV ou XML. Se eu tiver que carregar enormes quantidades de dados ... eu não. Você nunca precisa carregar enormes quantidades de dados :) Não para testes de unidade. Os testes de desempenho são outro problema e regras diferentes se aplicam.

Aaron Digulla
fonte
11
A velocidade é o único motivo para usar (especificamente) um banco de dados na memória?
rinogo 31/01
2
Eu acho que outra vantagem pode ser sua natureza "descartável" - não há necessidade de se limpar; basta matar o banco de dados na memória. (Mas há outras maneiras de fazer isso, como a abordagem ROLLBACK você mencionou)
rinogo
11
A vantagem é que cada teste pode escolher sua estratégia individualmente. Temos testes que fazem o trabalho em threads filho, o que significa que o Spring sempre confirmará os dados.
Aaron Digulla
@ Aaron: também estamos seguindo essa estratégia. Gostaria de saber qual é a sua estratégia para afirmar que o modelo em memória tem a mesma estrutura que o banco de dados real?
Guillaume
11
@ Guillaume: Estou criando todos os bancos de dados a partir dos mesmos arquivos SQL. O H2 é ótimo para isso, pois suporta a maioria das idiossincrasias SQL dos principais bancos de dados. Se isso não funcionar, eu uso um filtro que pega o SQL original e o converte no SQL para o banco de dados na memória.
Aaron Digulla
14

Estou fazendo essa pergunta há muito tempo, mas acho que não há uma bala de prata para isso.

O que atualmente faço é zombar dos objetos DAO e manter uma representação na memória de uma boa coleção de objetos que representam casos interessantes de dados que podem estar no banco de dados.

O principal problema que vejo com essa abordagem é que você está cobrindo apenas o código que interage com sua camada do DAO, mas nunca testando o próprio DAO e, na minha experiência, vejo que muitos erros também ocorrem nessa camada. Também mantenho alguns testes de unidade que são executados no banco de dados (por uma questão de usar TDD ou teste rápido localmente), mas esses testes nunca são executados no meu servidor de integração contínua, pois não mantemos um banco de dados para esse fim e eu acho que os testes executados no servidor de IC devem ser independentes.

Outra abordagem que acho muito interessante, mas nem sempre vale a pena, pois consome um pouco de tempo, é criar o mesmo esquema usado para produção em um banco de dados incorporado que é executado apenas no teste de unidade.

Embora não haja dúvida de que essa abordagem melhora sua cobertura, existem algumas desvantagens, pois você deve estar o mais próximo possível do ANSI SQL para que ele funcione tanto com o DBMS atual quanto com a substituição incorporada.

Não importa o que você ache mais relevante para o seu código, existem alguns projetos por aí que podem facilitar, como o DbUnit .

kolrie
fonte
13

Mesmo se existem ferramentas que permitem que você para zombar seu banco de dados, de uma forma ou de outra (por exemplo jOOQ 's MockConnection, o que pode ser visto em esta resposta - aviso legal, eu trabalho para o fornecedor de jOOQ), eu aconselharia não para zombar bancos de dados maiores com complexo consultas.

Mesmo se você quiser apenas testar a integração do seu ORM, lembre-se de que um ORM emite uma série muito complexa de consultas ao seu banco de dados, que pode variar em

  • sintaxe
  • complexidade
  • ordem (!)

Zombar de tudo isso para produzir dados fictícios sensíveis é bastante difícil, a menos que você esteja construindo um pequeno banco de dados dentro do seu mock, que interpreta as instruções SQL transmitidas. Dito isso, use um banco de dados de teste de integração conhecido que possa ser redefinido facilmente com dados conhecidos, nos quais você poderá executar seus testes de integração.

Lukas Eder
fonte
5

Eu uso o primeiro (executando o código em um banco de dados de teste). O único problema substantivo que vejo com essa abordagem é a possibilidade de os esquemas ficarem fora de sincronia, com os quais mantenho mantendo um número de versão no meu banco de dados e fazendo todas as alterações de esquema por meio de um script que aplica as alterações para cada incremento de versão.

Também faço todas as alterações (incluindo o esquema do banco de dados) no meu ambiente de teste, para que seja o contrário: depois que todos os testes forem aprovados, aplique as atualizações do esquema ao host de produção. Também mantenho um par separado de bancos de dados de teste versus aplicativo no meu sistema de desenvolvimento para poder verificar se a atualização do db funciona corretamente antes de tocar nas caixas de produção reais.

Dave Sherohman
fonte
3

Estou usando a primeira abordagem, mas um pouco diferente que permite resolver os problemas que você mencionou.

Tudo o que é necessário para executar testes para DAOs está no controle de origem. Inclui esquema e scripts para criar o banco de dados (a janela de encaixe é muito boa para isso). Se o banco de dados incorporado puder ser usado - eu o uso para acelerar.

A diferença importante com as outras abordagens descritas é que os dados necessários para o teste não são carregados de scripts SQL ou arquivos XML. Tudo (exceto alguns dados do dicionário que são efetivamente constantes) é criado pelo aplicativo usando funções / classes de utilitário.

O principal objetivo é tornar os dados usados ​​pelo teste

  1. muito perto do teste
  2. explícito (o uso de arquivos SQL para dados torna muito problemático ver que parte dos dados é usada por qual teste)
  3. isole os testes das alterações não relacionadas.

Basicamente, significa que esses utilitários permitem especificar declarativamente apenas coisas essenciais para o teste no próprio teste e omitir coisas irrelevantes.

Para dar uma idéia do que isso significa na prática, considere o teste para algum DAO que funciona com Comments para Posts escritos por Authors. Para testar operações CRUD para esse DAO, alguns dados devem ser criados no banco de dados. O teste seria semelhante a:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Isso tem várias vantagens sobre scripts SQL ou arquivos XML com dados de teste:

  1. Manter o código é muito mais fácil (adicionar uma coluna obrigatória, por exemplo, em alguma entidade mencionada em muitos testes, como o Author, não requer a alteração de muitos arquivos / registros, mas apenas uma alteração no construtor e / ou fábrica)
  2. Os dados exigidos por um teste específico são descritos no próprio teste e não em outro arquivo. Essa proximidade é muito importante para a compreensibilidade do teste.

Rollback vs Commit

Acho mais conveniente que os testes sejam confirmados quando executados. Em primeiro lugar, alguns efeitos (por exemplo DEFERRED CONSTRAINTS) não podem ser verificados se a confirmação nunca acontecer. Em segundo lugar, quando um teste falha, os dados podem ser examinados no banco de dados, pois não são revertidos pela reversão.

Por uma razão, isso tem uma desvantagem: o teste pode produzir dados quebrados e isso levará a falhas em outros testes. Para lidar com isso, tento isolar os testes. No exemplo acima, todo teste pode criar um novo Authore todas as outras entidades são criadas relacionadas a ele, portanto colisões são raras. Para lidar com os invariantes restantes que podem ser potencialmente interrompidos, mas não podem ser expressos como uma restrição de nível de banco de dados, uso algumas verificações programáticas para condições incorretas que podem ser executadas após cada teste (e são executadas no IC, mas geralmente desativadas localmente para desempenho) razões).

Roman Konoval
fonte
Se você propagar o banco de dados usando entidades e o orm, em vez de scripts sql, também terá a vantagem de o compilador forçar a correção do código de propagação, se você fizer alterações em seu modelo. Somente relevante se você usar uma linguagem estática de tipo, é claro.
daramasala 22/06/19
Então, para esclarecimentos: você está usando as funções / classes de utilitário em todo o aplicativo ou apenas para os testes?
Ella
@ Todas essas funções utilitárias geralmente não são necessárias fora do código de teste. Pense, por exemplo, sobre PostBuilder.post(). Ele gera alguns valores para todos os atributos obrigatórios da postagem. Isso não é necessário no código de produção.
Roman Konoval 18/03
2

Para projetos baseados em JDBC (direta ou indiretamente, por exemplo, JPA, EJB, ...), você pode simular não todo o banco de dados (nesse caso, seria melhor usar um banco de dados de teste em um RDBMS real), mas apenas simular no nível JDBC .

A vantagem é a abstração que vem dessa maneira, já que os dados JDBC (conjunto de resultados, contagem de atualizações, aviso, ...) são os mesmos, independentemente do back-end: seu prod db, um db de teste ou apenas alguns dados de maquete fornecidos para cada teste caso.

Com a conexão JDBC copiada para cada caso, não há necessidade de gerenciar o banco de dados de teste (limpeza, apenas um teste por vez, recarregar equipamentos, ...). Toda conexão de maquete é isolada e não há necessidade de limpeza. Somente dispositivos mínimos necessários são fornecidos em cada caso de teste para simular a troca JDBC, o que ajuda a evitar a complexidade de gerenciar um banco de dados inteiro de teste.

Acolyte é minha estrutura, que inclui um driver e utilitário JDBC para esse tipo de maquete: http://acolyte.eu.org .

cchantep
fonte