Qual é a melhor estratégia para aplicações orientadas a base de dados de testes de unidade?

Eu trabalho com um monte de aplicações web que são impulsionadas por bases de dados de complexidade variável na infra-estrutura. Normalmente, existe uma camadaORM separada da lógica de negócio e apresentação. Isso faz com que o teste de unidade da lógica de negócios seja bastante simples; as coisas podem ser implementadas em módulos discretos e quaisquer dados necessários para o teste podem ser falsificados através de zombação de objetos.

Mas testar a ORM e a própria base de dados sempre foi repleta de problemas e compromissos.

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

  • Carregue uma base de dados de testes com dados conhecidos. Execute testes com a ORM e confirme que os dados certos voltam. A desvantagem aqui é que o seu DB de teste tem que acompanhar todas as alterações do esquema no banco de dados da aplicação, e pode ficar fora de sincronia. Ele também se baseia em dados Artificiais, e pode não expor bugs que ocorrem devido à entrada estúpida do Usuário. Finalmente, se a base de dados de testes for pequena, não revelará ineficiências como um índice em falta. (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 da base de dados de produção e teste com base nisso. O problema aqui é que você pode não ter nenhuma idéia do que está no DB de produção a qualquer momento; seus testes podem precisar ser reescritos se os dados mudam ao longo do tempo.

Algumas pessoas salientaram que ambas as estratégias basear-se em dados específicos, e um teste unitário deve testar apenas a funcionalidade. Para esse fim, eu tenho visto sugerido:

  • Use um servidor de banco de dados mock, e verifique apenas se o ORM está a enviar as consultas correctas em resposta a uma chamada de método dada.

que estratégias usou para testar aplicações orientadas por bases de dados, se houver? O que funcionou melhor para ti?

Author: friedo, 2008-09-28

7 answers

Na verdade, usei a tua primeira abordagem com bastante sucesso, mas de uma forma ligeiramente diferente que acho que resolveria alguns dos teus problemas.
  1. Mantenha todo o esquema e scripts para criá-lo no controle de código para que qualquer um possa criar o esquema de banco de dados atual após uma verificação. Além disso, manter dados de amostra em arquivos de dados que são carregados por parte do processo de construção. À medida que você descobre dados que causam erros, adicione-o aos seus dados de amostra para verificar que os erros não reaparecem.

  2. Use um servidor de integração contínua para construir o esquema de banco de dados, carregar os dados da amostra e executar testes. É assim que mantemos a nossa base de dados de testes em sincronia (reconstruindo-a em cada teste). Embora isso exija que o servidor CI tenha acesso e propriedade de sua própria instância de banco de dados dedicada, eu digo que ter o nosso db schema construído 3 vezes por dia ajudou dramaticamente a encontrar erros que provavelmente não teriam sido encontrados até pouco antes da entrega (se não tarde). Não posso dizer que reconstruo o esquema antes de cada compromisso. Alguém sabe? Com esta abordagem você não terá que (bem talvez devêssemos, mas não é um grande negócio se alguém esquece).

  3. Para o meu grupo, a entrada do utilizador é feita ao nível da aplicação (não db), pelo que esta é testada através de testes unitários normalizados.

A Carregar A Base De Dados De Produção Cópia:
Esta foi a abordagem que foi usada no meu último trabalho. Foi uma grande dor causada por um par de questões:

  1. a cópia ficaria desactualizada a partir da versão de produção
  2. alterações seriam feitas no esquema da cópia e não seriam propagadas para os sistemas de produção. Nesta altura teríamos esquemas divergentes. Não é divertido.

A Gozar Com O Servidor De Bases De Dados:
Também fazemos isto no meu trabalho actual. Depois de cada commit executamos testes de unidade contra o código de aplicação que têm mock DB accessors injetados. Então, três vezes por dia executamos a construção completa do db descrever. Recomendo ambas as abordagens.

 160
Author: Mark Roddy, 2009-12-16 16:19:58
Estou sempre a fazer testes contra um DB na memória (HSQLDB ou Derby)por estas razões:
    Faz - nos pensar quais os dados a manter no nosso banco de ensaios e porquê. Apenas transportando seu DB de produção em um sistema de teste se traduz para " eu não tenho idéia do que estou fazendo ou por que e se algo quebra, não fui eu!!" ;)
  • Ele garante que a base de dados pode ser recriada com pouco esforço em um novo lugar (por exemplo, quando precisamos replicar um bug da produção)
  • Ajuda enormemente com a qualidade dos arquivos DDL.

O DB na memória é carregado com novos dados uma vez que os testes começam e depois da maioria dos testes, invoco ROLLBACK para mantê-lo estável. manter sempre Os dados no DB de ensaio estáveis! Se os dados mudam o tempo todo, você não pode testar.

Os dados são carregados a partir de SQL, um modelo DB ou um dump/backup. Eu prefiro dumps se eles estão em um formato legível porque eu posso colocá-los em VCS. Se isso não funcionar, eu uso um arquivo CSV ou XML. Se Eu temos de carregar enormes quantidades de dados ... Eu não. você nunca tem que carregar quantidades enormes de dados:) não para testes unitários. Os testes de desempenho são outra questão e aplicam-se regras diferentes.

 59
Author: Aaron Digulla, 2008-11-24 15:34:50
Tenho feito esta pergunta há muito tempo, mas acho que não há nenhuma bala de prata para isso. O que eu faço atualmente é zombar dos objetos DAO e manter uma representação em memória de uma boa coleção de objetos que representam casos interessantes de dados que poderiam viver na base de dados. O principal problema que eu vejo com essa abordagem é que você está cobrindo apenas o código que interage com a sua camada DAO, mas nunca testando o próprio DAO, e na minha experiência eu veja que muitos erros acontecem nessa camada também. Eu também manter alguns testes de unidade que executa o banco de dados (por causa do uso de TDD ou testes rápidos localmente), mas esses testes nunca são executados em meu servidor de integração contínua, uma vez que não manter um banco de dados para esse fim, e eu acho que os testes que são executados no CI servidor deve ser auto-contido. Outra abordagem que eu acho muito interessante, mas nem sempre vale a pena pois é um pouco demorado, é criar o mesmo esquema que você usa para produção em um banco de dados embutido que apenas corre dentro do teste da unidade.

Mesmo que não haja dúvida que esta abordagem melhora a sua cobertura, existem alguns inconvenientes, uma vez que você tem que estar o mais próximo possível do SQL ANSI para fazê-lo funcionar tanto com o seu DBMS atual e a substituição embutida.

Não importa o que você acha que é mais relevante para o seu código, existem alguns projetos lá fora que podem torná-lo mais fácil, como DbUnit.

 14
Author: kolrie, 2008-09-28 03:55:29

, Mesmo se existem ferramentas que permitem a simulação de seu banco de dados de uma forma ou de outra (por exemplo, jOOQ's MockConnection, o que pode ser visto em esta resposta - aviso, eu trabalho para jOOQ do fornecedor), aconselho - não a simulação de grandes bancos de dados com consultas complexas.

Mesmo que você só queira integração-teste o seu ORM, tenha cuidado que um ORM emite uma série muito complexa de consultas para o seu banco de dados, que pode variar em

  • sintaxe
  • complexidade
  • ordem (!)
Gozar com tudo isso para produzir dados sensatos é muito difícil, a não ser que esteja a construir uma pequena base de dados dentro da sua simulação, que interpreta as declarações SQL transmitidas. Tendo dito isso, use um banco de dados de testes de integração bem conhecido que você pode facilmente reiniciar com dados bem conhecidos, contra os quais você pode executar seus testes de integração.
 13
Author: Lukas Eder, 2017-05-23 12:26:23

USO o primeiro (a correr o código numa base de dados de testes). A única questão substantiva que eu vejo você levantando com esta abordagem é a possibilidade de esquemas ficando fora de sincronia, que eu lido com mantendo um número de versão em meu banco de dados e fazendo todas as alterações de esquema através 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 da base de dados) contra o meu ambiente de teste primeiro, por isso acaba por ser ao contrário: depois todos os testes passam, aplique as atualizações do esquema para a máquina de produção. Eu também manter um par separado de testing vs. application bases de dados em meu sistema de desenvolvimento para que eu possa verificar lá que a atualização db funciona corretamente antes de tocar a caixa de produção real(es).

 5
Author: Dave Sherohman, 2008-11-24 17:06:47
Estou a usar a primeira abordagem, mas um pouco diferente que permite resolver os problemas que mencionou. Tudo o que é necessário para fazer testes para a DAOs está no controle de origem. Ele inclui esquemas e scripts para criar o DB (docker é muito bom para isso). Se o DB incorporado pode ser usado-eu usá-lo para a velocidade.

A diferença importante com as outras abordagens descritas é que os dados necessários para o teste não são carregados a partir de scripts SQL ou ficheiros XML. Todo (exceto alguns dados do dicionário que é efetivamente constante)é criado por aplicação usando funções/classes de utilidade.

O principal objectivo é fazer com que os dados utilizados no ensaio

  1. Muito perto do teste
  2. explícito (usar ficheiros SQL para os dados torna muito problemático ver qual é o pedaço de dados usado pelo teste)
  3. Isola os testes das alterações não relacionadas.
Basicamente significa que estes utilitários permitem especificar declarativamente apenas coisas essencial para o teste em si mesmo e omitir coisas irrelevantes.

Para dar alguma ideia do que significa na prática, considere o teste para algum DAO que trabalha com Comment s a Posts escrito por Authors. A fim de testar as operações CRUD para tal DAO alguns dados devem ser criados no DB. O teste pareceria como:

@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));        
}

Isto tem várias vantagens sobre scripts SQL ou ficheiros XML com dados de teste:

  1. manter o código é muito mais fácil (adicionar uma coluna obrigatória para exemplo em alguma entidade que é referenciada em muitos testes, como o autor, não precisa mudar muitos arquivos/registros, mas apenas uma mudança 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 qualquer outro ficheiro. Esta proximidade é muito importante para a compreensibilidade do teste.

Rollback vs Commit

Acho mais conveniente que os testes se comprometam quando são executados. Em primeiro lugar, alguns efeitos (por exemplo DEFERRED CONSTRAINTS) não pode ser verificado se o commit nunca acontecer. Em segundo lugar, quando um ensaio falha, os dados podem ser examinados no DB, uma vez que não são revertidos pelo rollback. Isto tem uma desvantagem que o teste pode produzir dados quebrados e isso levará às falhas em outros testes. Para lidar com isto, tento isolar os testes. No exemplo acima, cada teste pode criar um novo Author e todas as outras entidades são criadas relacionadas a ele de modo que colisões são raras. Para lidar com os invariantes restantes que podem ser potencialmente quebrado, mas não pode ser expresso como uma restrição de nível DB eu uso algumas verificações programáticas para condições errôneas que podem ser executadas após cada teste único (e eles são executados em IC, mas geralmente desligados localmente por razões de desempenho).
 3
Author: Roman Konoval, 2019-03-06 12:32:27

Para o projecto baseado na JDBC (directa ou indirectamente, por exemplo, a APP, a EJB, etc ...) você pode mockup não todo o banco de dados (nesse caso, seria melhor usar um db de teste em um RDBMS real), mas apenas mockup ao nível do JDBC.

Vantagem é abstração que vem com esse caminho, como dados JDBC (conjunto de resultados, Contagem de atualização, aviso,...) são os mesmos o que quer que seja a infra-estrutura: o seu Prod db, um test db, ou apenas alguns modelos de dados fornecidos para cada caso de teste.

Com a ligação JDBC bloqueada para cada caso não há necessidade de gerenciar db de teste (limpeza, apenas um teste de cada vez, recarregar fixações, ...). Cada ligação de maquete é isolada e não há necessidade de limpar. Em cada caso de ensaio, são fornecidos apenas equipamentos mínimos necessários para maquilhar o JDBC exchange, o que ajuda a evitar a complexidade da Gestão de um db de ensaio completo.

Acólito é a minha estrutura que inclui um driver JDBC e um utilitário para este tipo de maquete: http://acolyte.eu.org

 2
Author: cchantep, 2018-11-28 21:07:36