Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Como discutido na Visão Geral, uma decisão básica que precisa de tomar é se os seus testes vão envolver o seu sistema de base de dados de produção – tal como a sua aplicação – ou se os seus testes irão correr contra um teste duplo, que substitui o seu sistema de base de dados de produção.
Testar contra um recurso externo real – em vez de o substituir por um duplo de teste – pode envolver as seguintes dificuldades:
- Em muitos casos, simplesmente não é possível ou prático testar contra o recurso externo real. Por exemplo, a sua aplicação 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).
- Mesmo quando é possível envolver o recurso externo real, isso pode ser extremamente lento: executar uma grande quantidade de testes num serviço cloud pode fazer com que os testes demore demasiado tempo. Os testes devem fazer parte do fluxo de trabalho diário do programador, por isso é importante que os testes corram rapidamente.
- Executar testes contra um recurso externo pode envolver problemas de isolamento, onde os testes interferem entre si. Por exemplo, múltiplos testes a correr em paralelo contra uma base de dados podem modificar dados e causar falhas mútuas de várias formas. Usar um teste duplo evita isto, pois cada teste corre contra o seu próprio recurso em memória, estando assim naturalmente isolado dos outros testes.
No entanto, os testes que passam contra um teste duplo não garantem que o seu programa funcione quando está a correr contra o recurso externo real. Por exemplo, um elemento de teste da base de dados pode realizar comparações de cadeias que distinguem entre maiúsculas e minúsculas, enquanto o sistema de gestão de base de dados de produção faz comparações que não distinguem entre maiúsculas e minúsculas. Tais problemas só são detetados quando testes são executados contra a sua base de dados real de produção, tornando estes testes uma parte importante de qualquer estratégia de testes.
Testar contra a base de dados pode ser mais fácil do que parece
Devido às dificuldades acima referidas em testar contra uma base de dados real, os programadores são frequentemente aconselhados a usar primeiro os testes duplos e a dispor de um conjunto de testes robusto que possam executar frequentemente nas suas máquinas; Testes envolvendo a base de dados, em contraste, supostamente são executados com muito menos frequência e, em muitos casos, também oferecem muito menos cobertura. Recomendamos pensar mais nesta última hipótese e sugerimos que as bases de dados podem, na verdade, ser muito menos afetadas pelos problemas acima do que as pessoas tendem a pensar:
- Atualmente, a maioria das bases de dados pode ser facilmente instalada na máquina do programador. Tecnologias baseadas em containers como o Docker podem tornar isto muito fácil, e bibliotecas como as Testcontainers podem ajudar a automatizar o ciclo de vida das bases de dados containerizadas nos seus testes. Tecnologias como o GitHub Workspaces e o Dev Container configuram todo o seu ambiente de desenvolvimento para si (incluindo a base de dados). Ao usar SQL Server, também é possível testar contra o LocalDB no Windows, ou configurar facilmente uma imagem Docker no Linux.
- Testar contra uma base de dados local – com um conjunto de dados de teste razoável – é geralmente extremamente rápido: a comunicação é completamente local e os dados de teste são tipicamente armazenados em memória do lado da base de dados. O próprio EF Core contém mais de 30.000 testes apenas contra SQL Server; estes terminam de forma fiável em poucos minutos, executam-se em CI em cada commit e são muito frequentemente executados por programadores locais. Alguns programadores recorrem a uma base de dados em memória (uma "falsa") acreditando que isso é necessário para a velocidade – o que quase nunca acontece realmente.
- O isolamento é, de facto, um obstáculo ao executar testes contra uma base de dados real, pois os testes podem modificar dados e interferir uns com os outros. No entanto, existem várias técnicas para proporcionar isolamento em cenários de testes de bases de dados; concentramo-nos nestes em Testes contra o seu sistema de base de dados de produção).
O exposto acima não pretende menosprezar os duplos de teste nem argumentar contra o seu uso. Por um lado, os doubles de teste são necessários para alguns cenários que não podem ser testados de outra forma, como simular falhas na base de dados. No entanto, na nossa experiência, os utilizadores frequentemente evitam testar contra a sua base de dados pelos motivos acima, acreditando que é lento, difícil ou pouco fiável, quando isso não é necessariamente verdade. Testar contra o seu sistema de base de dados de produção visa resolver isto, fornecendo diretrizes e exemplos para escrever testes rápidos e isolados contra a sua base de dados.
Diferentes tipos de dublês de teste
Testes duplos é um termo amplo que engloba abordagens muito diferentes. Esta secção aborda algumas técnicas comuns que envolvem duplicações de teste para testar aplicações do EF Core:
- Use o SQLite (modo em memória) como uma base de dados falsa, substituindo o seu sistema de base de dados de produção.
- Utilize o provedor EF Core em memória como um simulador de base de dados, para substituir o seu sistema de base de dados de produção.
- Simule ou substitua
DbContexteDbSet. - Introduza uma camada de repositório entre o EF Core e o código da sua aplicação, e faça mock ou stub dessa camada.
Abaixo, vamos explorar o significado de cada método e compará-lo com os outros. Recomendamos a leitura dos diferentes métodos para obter uma compreensão completa de cada um. Se decidiu escrever testes que não envolvem o seu sistema de base de dados de produção, então uma camada repositório é a única abordagem que permite o stubbing/mocking abrangente e fiável da camada de dados. No entanto, essa abordagem tem um custo significativo em termos de implementação e manutenção.
SQLite como uma base de dados falsa
Uma possível abordagem de teste é trocar a base de dados de produção (e.g. SQL Server) por SQLite, usando-a efetivamente como uma simulação de teste. Para além da facilidade de configuração, o SQLite tem uma funcionalidade de base de dados em memória que é especialmente útil para testes: cada teste está naturalmente isolado na sua própria base de dados em memória, e não é necessário gerir ficheiros reais.
No entanto, antes de fazer isto, é importante compreender que no EF Core, diferentes fornecedores de bases de dados comportam-se de forma distinta – o EF Core não tenta abstrair todos os aspetos do sistema de base de dados subjacente. Fundamentalmente, isto significa que testar contra SQLite não garante os mesmos resultados que contra SQL Server ou qualquer outra base de dados. Aqui estão alguns exemplos de possíveis diferenças comportamentais:
- A mesma consulta LINQ pode devolver resultados diferentes em diferentes fornecedores. Por exemplo, o SQL Server faz comparação de cadeias de caracteres sem distinção entre maiúsculas e minúsculas por padrão, enquanto o SQLite distingue entre maiúsculas e minúsculas. Isto pode fazer com que os teus testes passem contra SQLite, onde falhariam contra SQL Server (ou vice-versa).
- Algumas consultas que funcionam no SQL Server simplesmente não são suportadas no SQLite, porque o suporte exato ao SQL nestas duas bases de dados é diferente.
- Se a tua consulta usar um método específico do fornecedor, como o do
EF.Functions.DateDiffDaySQL Server, essa consulta falhará no SQLite e não poderá ser testada. - O SQL bruto pode funcionar, ou pode falhar ou devolver resultados diferentes, dependendo exatamente do que está a ser feito. Os dialetos SQL diferem em muitos aspetos entre bases de dados.
Comparado com fazer testes contra o seu sistema de base de dados de produção, é relativamente fácil começar com SQLite, e muitos utilizadores fazem-no. Infelizmente, as limitações acima tendem a tornar-se problemáticas ao testar aplicações EF Core, mesmo que não pareçam ser no início. Por isso, recomendamos que escreva os seus testes contra a sua base de dados real ou, se usar um duplo teste for absolutamente necessário, assumir o custo de um padrão de repositório conforme discutido abaixo.
Para informações sobre como usar SQLite para testes, consulte esta secção.
Na memória como uma base de dados falsa
Como alternativa ao SQLite, o EF Core inclui também um provedor em memória. Embora este fornecedor tenha sido originalmente concebido para suportar testes internos do próprio EF Core, alguns programadores utilizam-no como uma falsificação de base de dados ao testar aplicações EF Core. Fazê-lo é altamente desaconselhado: sendo uma base de dados falsa, a inmemory tem os mesmos problemas que o SQLite (ver acima), mas além das seguintes limitações adicionais:
- O fornecedor em memória geralmente suporta menos tipos de consulta do que o fornecedor SQLite, uma vez que não é uma base de dados relacional. Mais consultas falham ou comportam-se de forma diferente em comparação com a tua base de dados de produção.
- As transações não são suportadas.
- SQL bruto é completamente sem suporte. Compare isto com o SQLite, onde é possível usar SQL bruto, desde que esse SQL funcione da mesma forma no SQLite e na sua base de dados de produção.
- O fornecedor em memória não foi otimizado para desempenho e geralmente trabalha mais devagar do que o SQLite em modo em memória (ou mesmo no seu sistema de base de dados de produção).
Em resumo, a in-memory tem todas as desvantagens do SQLite, juntamente com algumas mais – e não oferece vantagens em troca. Se está à procura de uma base de dados simples simulada em memória, use o SQLite em vez do provedor em memória; contudo, considere utilizar o padrão repositório conforme descrito abaixo.
Para informações sobre como usar o in-memory para testes, consulte esta secção.
Mocking ou stubbing DbContext e DbSet
Esta abordagem utiliza tipicamente uma estrutura simulada para criar um duplo de teste de DbContext e DbSet, e testa contra esses duplos. Mocking DbContext pode ser uma boa abordagem para testar várias funcionalidades não relacionadas com consultas , como chamadas para Add ou SaveChanges(), permitindo-lhe verificar que o seu código as chamou em cenários de escrita.
No entanto, simular corretamente a DbSet não é possível, uma vez que as consultas são expressas através de operadores LINQ, que são chamadas estáticas de método de extensão sobre . Como resultado, quando algumas pessoas falam em "mocking DbSet", o que realmente querem dizer é que criam um DbSet suportado por uma coleção em memória, e depois aplicam operadores de consulta a essa coleção na memória, assim como num simples IEnumerable. Em vez de um mock, isto é na verdade uma espécie de falsidade, onde a coleção em memória substitui a base de dados real.
Como apenas o DbSet próprio é falsificado e a consulta é avaliada em memória, esta abordagem acaba por ser muito semelhante à utilização do fornecedor EF Core em memória: ambas as técnicas executam operadores de consulta em .NET sobre uma coleção em memória. Como resultado, esta técnica sofre das mesmas desvantagens: as consultas comportam-se de forma diferente (por exemplo, em torno da sensibilidade a maiúsculas minúsculas) ou simplesmente falham (por exemplo, devido a métodos específicos do fornecedor), SQL puro não funciona e as transações serão, na melhor das hipóteses, ignoradas. Como resultado, esta técnica deve ser geralmente evitada para testar qualquer código de consulta.
Padrão do repositório
As abordagens acima tentaram trocar o provedor de base de dados de produção do EF Core por um provedor de testes falso, ou criar um DbSet suportado por uma coleção em memória. Estas técnicas são semelhantes no sentido em que ainda avaliam as consultas LINQ do programa – quer em SQLite, quer em memória – e esta é, em última análise, a origem das dificuldades acima descritas: uma consulta concebida para ser executada contra uma base de dados de produção específica não pode ser executada de forma fiável noutro local sem problemas.
Para um teste duplo adequado e fiável, considere introduzir uma camada de repositório que intermedie o código da sua aplicação e o EF Core. A implementação de produção do repositório contém as consultas LINQ reais e executa-as via EF Core. Nos testes, a abstração do repositório é diretamente substituída ou simulada sem necessidade de consultas reais ao LINQ, eliminando efetivamente o EF Core da sua stack de testes e permitindo que os testes se concentrem apenas no código da aplicação.
O diagrama seguinte compara a abordagem simulada da base de dados (SQLite/em memória) com o padrão de repositório.
Como as consultas LINQ já não fazem parte dos testes, pode fornecer diretamente os resultados das consultas à sua aplicação. Por outras palavras, as abordagens anteriores permitiam aproximadamente eliminar entradas de consulta (por exemplo, substituir tabelas SQL Server por tabelas em memória), mas ainda assim executar os operadores de consulta em memória. O padrão de repositório, em contraste, permite simular diretamente os resultados das consultas, permitindo testes unitários muito mais poderosos e focados. Tenha em consideração que, para que isto funcione, seu repositório não deve expor métodos que retornem IQueryable, pois estes não podem ser simulados novamente; o IEnumerable deve ser utilizado em vez disso.
No entanto, uma vez que o padrão do repositório requer encapsular cada uma das consultas LINQ (testáveis) num método IEnumerable-returning, impõe uma camada arquitetónica adicional à sua aplicação e pode incorrer em custos significativos para implementar e manter. Este custo não deve ser descontado ao escolher como testar uma aplicação, especialmente tendo em conta que provavelmente continuam a ser necessários testes contra a base de dados real para as consultas expostas pelo repositório.
Vale a pena notar que os repositórios têm vantagens para além do simples teste. Garantem que todo o código de acesso aos dados está concentrado num só local em vez de estar espalhado pela aplicação, e se a sua aplicação precisar de suportar mais do que uma base de dados, a abstração do repositório pode ser muito útil para ajustar consultas entre fornecedores.
Para um exemplo que mostra testes com um repositório, consulte esta secção.
Comparação geral
A tabela seguinte apresenta uma visão rápida e comparativa das diferentes técnicas de teste e mostra que funcionalidades podem ser testadas sob qual abordagem:
| Característica | Dentro da memória | SQLite na memória | Simulação de DbContext | Padrão do repositório | Testes contra a base de dados |
|---|---|---|---|---|---|
| Tipo duplo de teste | Falso | Falso | Falso | Mock/esboço | Real, sem duplo |
| SQL bruto? | Não | Depends | Não | Yes | Yes |
| Transações? | Não (ignorado) | Yes | Yes | Yes | Yes |
| Traduções específicas para cada fornecedor? | Não | Não | Não | Yes | Yes |
| Comportamento exato da consulta? | Depends | Depends | Depends | Yes | Yes |
| É possível usar o LINQ em qualquer parte da aplicação? | Yes | Yes | Yes | Não* | Yes |
* Todas as consultas LINQ testáveis na base de dados devem ser encapsuladas em métodos de repositório que retornam IEnumerable, para serem simuladas/testadas.
Resumo
- Recomendamos que os desenvolvedores tenham uma boa cobertura de testes da sua aplicação executada contra o sistema real de base de dados de produção deles. Isto dá confiança de que a aplicação funciona realmente em produção e, com um design adequado, os testes podem ser executados de forma fiável e rápida. Como estes testes são obrigatórios de qualquer forma, é boa ideia começar por aí e, se necessário, adicionar testes usando testes duplos mais tarde, conforme necessário.
- Se decidiu usar uma duplicata de teste, recomendamos implementar o padrão de repositório, que lhe permite substituir ou simular a sua camada de acesso a dados sobre o EF Core, em vez de usar um provedor EF Core falso (Sqlite/in-memory) ou simular
DbSet. - Se o padrão de repositório não for uma opção viável por algum motivo, considere usar bases de dados SQLite em memória.
- Evite o fornecedor em memória para fins de teste – isto é desencorajado e só suportado para aplicações legadas.
- Evita simulações
DbSetpara propósito de consultas.