Testar contra a base: contentor ou memória?
A pergunta aparece sempre à mesma altura: chegou o momento de testar o código que fala com a base, e há que decidir contra o quê. As respostas habituais são três — uma base em memória, um duplo escrito à mão, ou a coisa a sério num contentor.
1. O argumento que decide
É este, e resolve a maior parte dos casos: se o que se quer testar é o SQL, então o SQL tem de correr no motor a sério.
Trocar o Postgres por um SQLite em memória parece o atalho perfeito, e o que se está a fazer é substituir aquilo que se queria verificar. Não é uma diferença de detalhe:
Tipos diferentes — não há
jsonb, não há arrays, não há tipos de intervalo.Comparações diferentes — o
ILIKEnão existe, e a sensibilidade a maiúsculas não é a mesma.Restrições diferentes — chaves estrangeiras que por omissão nem sequer são verificadas.
Transações diferentes, que é onde mais custa: os níveis de isolamento não coincidem, e o comportamento sob concorrência também não.
Uma consulta pode passar no substituto e partir em produção — e parte exatamente naquilo que motivou o teste.
2. «Mas é lento»
Era. Um Postgres em contentor arranca em poucos segundos e fica de pé durante a bateria toda. O que tem de ser rápido não é o arranque — é o isolamento entre testes, e aí há uma técnica que custa quase nada:
// Cada teste corre dentro de uma transação que nunca é confirmada
beforeEach(async () => {
await base.query('BEGIN');
});
afterEach(async () => {
await base.query('ROLLBACK');
});O rollback é imediato e não deixa rasto — muito mais barato do que apagar tabelas, e muito mais seguro do que confiar que cada teste limpa o que sujou.
Tem um limite que convém conhecer: código que abra transações suas obriga a usar pontos de salvaguarda, ou a trocar esta técnica por um TRUNCATE das tabelas entre testes.
3. Correr as migrações, não o esquema
A tentação é montar a base de teste a partir de um dump do esquema. Compensa mais aplicar as migrações, pela mesma ordem que elas correm em produção — passa a haver um teste, de graça, a garantir que elas aplicam sem erro numa base vazia.
4. E o duplo escrito à mão?
Tem lugar, mas noutro sítio. Serve para testar quem usa o repositório, não o repositório em si:
A lógica que decide o que fazer com os dados — contra um duplo em memória, rápido e sem cerimónia.
As consultas propriamente ditas — contra a base a sério, e só essas.
Assim ficam poucos testes lentos e muitos rápidos, em vez de uma bateria inteira presa a um contentor.
5. O que não deve acontecer
Uma base partilhada entre quem desenvolve. É a receita dos testes intermitentes — dois computadores a correr a mesma bateria ao mesmo tempo, a pisarem-se nas mesmas tabelas. É o caso que descrevi nos testes que falham de vez em quando, e aqui é auto-infligido.
Apontar os testes para a base de produção. Parece óbvio que não, e acontece — normalmente através de um .env que alguém copiou sem olhar.
6. E quando não é preciso nada disto
Vale a pena dizer, porque é o caso deste site: a bateria não toca na base. Não foi decisão de testes — foi consequência de a lógica que interessa ter ficado em funções que recebem valores e devolvem valores.
Antes de montar a infraestrutura toda, é uma pergunta que se poupa tempo a fazer: quanto do que quero testar é mesmo sobre a base? Muitas vezes é bem menos do que parecia, e o resto sai para funções que não precisam de nada.
O resumo
Se o teste é sobre uma consulta, a base a sério — em contentor, com as migrações aplicadas, uma transação por teste e um rollback no fim. Se é sobre a decisão que se toma com os dados, um duplo em memória chega e é mais rápido. O que não serve é um motor diferente a fingir que é o mesmo.