Escolha de uma estratégia de teste

Conforme discutido na Visão geral, uma decisão básica que você precisa tomar é se os testes envolverão o sistema de banco de dados de produção, assim como o aplicativo, ou se os testes serão executados em um teste duplo, que substitui o sistema de banco de dados de produção.

Teste com um recurso externo real, em vez de substituí-lo por um teste duplo, pode envolver as seguintes dificuldades:

  1. Em muitos casos, simplesmente não é possível ou prático fazer testes com o recurso externo real. Por exemplo, seu aplicativo pode interagir com algum serviço que não pode ser facilmente testado (devido à limitação de taxa ou à falta de um ambiente de teste).
  2. Mesmo quando é possível envolver o recurso externo real, isso pode ser extremamente lento: a execução de uma grande quantidade de testes em um serviço de nuvem pode fazer com que os testes demorem muito. O teste deve fazer parte do fluxo de trabalho diário do desenvolvedor, portanto, é importante que os testes sejam executados rapidamente.
  3. A execução de testes em um recurso externo pode envolver problemas de isolamento, em que os testes interferem uns nos outros. Por exemplo, vários testes executados em paralelo em um banco de dados podem modificar dados e causar falhas uns nos outros de várias maneiras. O uso de um teste duplo evita isso, pois cada teste é executado em seu próprio recurso na memória e, portanto, é naturalmente isolado de outros testes.

No entanto, os testes que passam um teste duplo não garantem que seu programa funcione quando executado no recurso externo real. Por exemplo, um banco de dados de teste duplo pode realizar comparações de cadeia de caracteres que diferenciam maiúsculas de minúsculas, enquanto o sistema de banco de dados de produção faz comparações que não diferenciam maiúsculas de minúsculas. Esses problemas só são descobertos quando os testes são executados em seu banco de dados de produção real, o que torna esses testes uma parte importante de qualquer estratégia de teste.

Testar o banco de dados pode ser mais fácil do que parece

Devido às dificuldades acima com os testes em um banco de dados real, os desenvolvedores são frequentemente incentivados a usar primeiro os testes duplos e a ter um conjunto de testes robusto que possa ser executado com frequência em seus computadores; os testes que envolvem o banco de dados, por outro lado, devem ser executados com muito menos frequência e, em muitos casos, também oferecem uma cobertura muito menor. Recomendamos que se reflita mais sobre o último e sugerimos que os bancos de dados podem, na verdade, ser muito menos afetados pelos problemas acima do que as pessoas tendem a pensar:

  1. Atualmente, a maioria dos bancos de dados pode ser facilmente instalada no computador do desenvolvedor. Tecnologias baseadas em contêiner, como o Docker, podem tornar isso muito fácil, e tecnologias como o Github Workspaces e oDev Container configuram todo o ambiente de desenvolvimento para você (inclusive o banco de dados). Ao usar o SQL Server, também é possível testar oLocalDB no Windows ou configurar facilmente uma imagem do Docker no Linux.
  2. Os testes em um banco de dados local, com um conjunto de dados de teste razoável, geralmente são extremamente rápidos: a comunicação é totalmente local e os dados de teste geralmente são armazenados em buffer na memória do banco de dados. O próprio EF Core contém mais de 30.000 testes somente em relação ao SQL Server; esses testes são concluídos de forma confiável em poucos minutos, são executados na CI em cada confirmação e são executados com muita frequência pelos desenvolvedores localmente. Alguns desenvolvedores recorrem a um banco de dados na memória (um "falso"), acreditando que isso é necessário para aumentar a velocidade - quase nunca é esse o caso.
  3. O isolamento é de fato um obstáculo ao executar testes em um banco de dados real, pois os testes podem modificar os dados e interferir uns nos outros. No entanto, há várias técnicas para fornecer isolamento em cenários de teste de banco de dados; concentramos-nos nelas em Testes em relação ao seu sistema de banco de dados de produção.

O que foi dito acima não tem a intenção de menosprezar os testes duplos nem de argumentar contra seu uso. Por um lado, os testes duplos são necessários para alguns cenários que não podem ser testados de outra forma, como a simulação de falhas no banco de dados. No entanto, em nossa experiência, os usuários frequentemente evitam fazer testes em seus bancos de dados pelos motivos acima, acreditando que são lentos, difíceis ou não confiáveis, quando esse não é necessariamente o caso. O teste em relação ao seu sistema de banco de dados de produção pretende resolver isso, fornecendo diretrizes e amostras para criação de testes rápidos e isolados em relação ao seu banco de dados.

Diferentes tipos de testes duplos

Testes duplos é um termo amplo que engloba abordagens muito diferentes. Esta seção aborda algumas técnicas comuns que envolvem testes duplos para testar aplicativos do EF Core:

  1. Use o SQLite (modo in-memory) como um banco de dados falso, substituindo o sistema de banco de dados de produção.
  2. Use o SQLite (modo in-memory) como um banco de dados falso, substituindo o sistema de banco de dados de produção.
  3. Faça uma simulação ou processamento de stub de DbContext e DbSet.
  4. Introduza uma camada de repositório entre o EF Core e o código do aplicativo e simule ou faça um stub dessa camada.

A seguir, exploraremos o significado de cada método e o compararemos com os demais. Recomendamos a leitura dos diferentes métodos para obter uma compreensão completa de cada um deles. Se você decidiu gravar testes que não envolvam o sistema de banco de dados de produção, uma camada de repositório é a única abordagem que permite o uso abrangente e confiável de simulação/stub da camada de dados. No entanto, essa abordagem tem um custo significativo em termos de implementação e manutenção.

SQLite como um banco de dados falso

Uma possível abordagem de teste é trocar seu banco de dados de produção (por exemplo, SQL Server) pelo SQLite, usando-o efetivamente como um teste "falso". Além da facilidade de instalação, o SQLite tem um recurso de banco de dados in-memory que é especialmente útil para testes: cada teste é naturalmente isolado em seu próprio banco de dados in-memory, e nenhum arquivo real precisa ser gerenciado.

No entanto, antes de fazer isso, é importante entender que, no EF Core, diferentes provedores de banco de dados se comportam de forma diferente - o EF Core não tenta abstrair todos os aspectos do sistema de banco de dados subjacente. Fundamentalmente, isso significa que os testes com o SQLite não garantem os mesmos resultados que o SQL Server ou qualquer outro banco de dados. Aqui estão alguns exemplos de possíveis diferenças de comportamento:

  • A mesma consulta LINQ pode retornar resultados diferentes em provedores diferentes. Por exemplo, o SQL Server faz a comparação de cadeias de caracteres que não diferencia maiúsculas de minúsculas por padrão, enquanto o SQLite diferencia maiúsculas de minúsculas. Isso pode fazer com que seus testes sejam aprovados no SQLite, mas falhariam no SQL Server (ou vice-versa).
  • Algumas consultas que funcionam no SQL Server simplesmente não são compatíveis com o SQLite, porque o suporte exato ao SQL nesses dois bancos de dados é diferente.
  • Se a sua consulta usar um método específico do provedor, como o EF.Functions.DateDiffDaydo SQL Server, essa consulta falhará no SQLite e não poderá ser testada.
  • O SQL bruto pode funcionar, falhar ou retornar resultados diferentes, dependendo exatamente do que está sendo feito. Os dialetos SQL são diferentes de várias maneiras entre os bancos de dados.

Em comparação com a execução de testes em seu sistema de banco de dados de produção, é relativamente fácil começar a usar o SQLite, e muitos usuários o fazem. Infelizmente, as limitações acima tendem a se tornar problemáticas ao testar aplicativos do EF Core, mesmo que não pareçam ser no início. Como resultado, recomendamos que você grave seus testes em seu banco de dados real ou, se o uso de um teste duplo for uma necessidade absoluta, que você leva em conta o custo de um padrão de repositório, conforme discutido abaixo.

Para obter informações sobre como usar o SQLite para testes, consulte esta seção.

In-memory como um banco de dados falso

Como alternativa ao SQLite, o EF Core também vem com um provedor de in-memory. Embora esse provedor tenha sido originalmente projetado para dar suporte a testes internos do próprio EF Core, alguns desenvolvedores o utilizam como um banco de dados falso ao testar aplicativos do EF Core. Fazer isso é altamente desencorajado: como um banco de dados falso, o in-memory tem os mesmos problemas que o SQLite (veja acima), mas além disso tem as seguintes limitações adicionais:

  • O provedor de in-memory geralmente oferece suporte a menos tipos de consulta do que o provedor SQLite, pois não é um banco de dados relacional. Mais consultas falharão ou se comportarão de forma diferente em comparação com seu banco de dados de produção.
  • As transações não são suportadas.
  • O SQL bruto não tem suporte algum. Compare isso com o SQLite, onde é possível usar SQL bruto, desde que esse SQL funcione da mesma forma no SQLite e no seu banco de dados de produção.
  • O provedor de in-memory não foi otimizado para desempenho e, em geral, funcionará mais lentamente do que o SQLite no modo in-memory (ou mesmo do que o seu sistema de banco de dados de produção).

Em resumo, o in-memory tem todas as desvantagens do SQLite, além de algumas outras, e não oferece nenhuma vantagem em troca. Se estiver procurando um simples banco de dados in-memory falso, use o SQLite em vez do provedor de in-memory, mas considere usar o padrão de repositório, conforme descrito abaixo.

Para obter informações sobre como usar o in-memory para testes, consulte esta seção.

Simulação ou stub de DbContext e DbSet

Essa abordagem normalmente usa uma estrutura de simulação para criar um teste duplo de DbContext e DbSet, e testá-los. A simulação de DbContext pode ser uma boa abordagem para testar várias funcionalidades de não consulta, como chamadas para Add ou SaveChanges(), permitindo que você verifique se o seu código as chamou em cenários de gravação.

No entanto, não é possível fazer a simulação adequada da funcionalidade de DbSetconsulta, pois as consultas são expressas por meio de operadores LINQ, que são chamadas de métodos de extensão estática em IQueryable. Como resultado, quando algumas pessoas falam sobre "simulação de DbSet", o que elas realmente querem dizer é que criam um DbSet apoiado por uma coleção in-memory e, em seguida, avaliam os operadores de consulta em relação a essa coleção in-memory, exatamente como um simples IEnumerable. Em vez de uma simulação, trata-se, na verdade, uma espécie de falsificação, em que a coleção in-memory substitui o banco de dados real.

Como apenas o DbSet em si é falso e a consulta é avaliada in-memory, essa abordagem acaba sendo muito semelhante ao uso do provedor de in-memory do EF Core: ambas as técnicas executam operadores de consulta no .NET em uma coleção in-memory. Como resultado, essa técnica também apresenta as mesmas desvantagens: as consultas se comportarão de forma diferente (por exemplo, em relação a maiúsculas e minúsculas) ou simplesmente falharão (por exemplo, devido a métodos específicos do provedor), o SQL bruto não funcionará e as transações serão ignoradas, na melhor das hipóteses. Como resultado, essa técnica geralmente deve ser evitada para testar qualquer código de consulta.

Padrão de Repositório

As abordagens acima tentaram trocar o provedor de banco de dados de produção do EF Core por um provedor de teste falso ou criar um DbSet apoiado por uma coleção in-memory. Essas técnicas são semelhantes no sentido de que ainda avaliam as consultas LINQ do programa, seja no SQLite ou no in-memory,e essa é, em última análise, a fonte das dificuldades descritas acima: uma consulta projetada para ser executada em um banco de dados de produção específico não pode ser executada de forma confiável em outro lugar sem problemas.

Para obter um teste duplo adequado e confiável, considere a introdução de uma camada de repositório que faça a mediação entre o código do aplicativo e o EF Core. A implementação de produção do repositório contém as consultas LINQ reais e as executa por meio do EF Core. Nos testes, a abstração do repositório é diretamente feita por meio de stub ou simulação sem a necessidade de consultas LINQ reais, removendo efetivamente o EF Core de sua pilha de testes e permitindo que os testes se concentrem apenas no código do aplicativo.

O diagrama a seguir compara a abordagem de banco de dados falso (SQLite/in-memory) com o padrão do repositório:

Comparison of fake provider with repository pattern

Como as consultas LINQ não fazem mais parte dos testes, você pode fornecer diretamente os resultados da consulta ao seu aplicativo. Em outras palavras, as abordagens anteriores permitem, aproximadamente, o stub de entradas de consulta (por exemplo, a substituição de tabelas do SQL Server por tabelas in-memory), mas ainda executam os operadores de consulta reais na memória. O padrão de repositório, por outro lado, permite que você processe stub das saídas de consulta diretamente, possibilitando testes de unidade muito mais avançados e focados. Observe que, para que isso funcione, seu repositório não pode expor nenhum método que retorne IQueryable, já que, mais uma vez, eles não podem processar stub; em vez disso, deve ser retornado IEnumerable.

No entanto, como o padrão de repositório exige o encapsulamento de toda e qualquer consulta LINQ (testável) em um método que retorna IEnumerable, ele impõe uma camada de arquitetura adicional ao seu aplicativo e pode gerar custos significativos de implementação e manutenção. Esse custo não deve ser desconsiderado ao escolher como testar um aplicativo, especialmente porque é provável que ainda sejam necessários testes no banco de dados real para as consultas expostas pelo repositório.

Vale a pena observar que os repositórios têm vantagens além dos testes. Eles garantem que todo o código de acesso a dados fique concentrado em um único local, em vez de ficar espalhado pelo aplicativo e, se o seu aplicativo precisar oferecer suporte a mais de um banco de dados, a abstração do repositório poderá ser muito útil para ajustar as consultas entre os provedores.

Para ver uma amostra de teste com um repositório, consulte esta seção.

Comparação geral

A tabela a seguir fornece uma visão rápida e comparativa das diferentes técnicas de teste e mostra qual funcionalidade pode ser testada em cada abordagem:

Recurso Na memória SQLite in-memory Simulação de DbContext Padrão de Repositório Testes no banco de dados
Tipos de testes duplos Falso Falso Falso Simulação/stub Real, não duplo
SQL bruto? Não Depende Não Sim Yes
Transações? Não (ignorado) Sim Sim Sim Yes
Traduções específicas do provedor? Não Número Não Sim Yes
Comportamento exato da consulta? Depende Depende Depende Sim Yes
É possível usar o LINQ em qualquer lugar do aplicativo? Sim Sim Yes Não* Sim

* Todas as consultas LINQ de banco de dados testáveis devem ser encapsuladas em métodos de repositório que retornam IEnumerable, para poderem ser processadas por stub/simulação.

Resumo

  • Recomendamos que os desenvolvedores tenham uma boa cobertura de testes de seus aplicativos em execução no sistema de banco de dados de produção real. Isso proporciona a confiança de que o aplicativo realmente funciona na produção e, com o design adequado, os testes podem ser executados de forma confiável e rápida. Como esses testes são necessários em qualquer caso, é uma boa ideia começar por aí e, se necessário, adicionar testes usando testes duplos posteriormente, conforme necessário.
  • Se você decidiu usar um teste duplo, recomendamos a implementação do padrão de repositório, que permite que você faça stub ou simulação da sua camada de acesso a dados acima do EF Core, em vez de usar um provedor falso de EF Core (Sqlite/in-memory) ou fazer uma simulação de DbSet.
  • Se o padrão de repositório não for uma opção viável por algum motivo, considere o uso de bancos de dados de SQLite in-memory.
  • Evite o provedor in-memory para fins de teste - isso não é recomendado e só é compatível com aplicativos herdados.
  • Evite a simulação de DbSet para fins de consulta.