Como desbloqueei testes que travavam no SQLite :memory: com AdonisJS
O problema
Estava implementando uma feature nova no backend (import de base de conhecimento do Freshdesk) e os testes funcionais passaram a falhar no setup — antes mesmo de rodar qualquer teste. O seed tentava inserir registros e recebia table tickets has no column named type. Parecia que a migration que adicionava a coluna não estava executando. Mas todas as migrations anteriores funcionavam. O que mudou?
A resposta esteve escondida no MigrationRunner do AdonisJS Lucid.
O que descobri sobre o MigrationRunner
Ao ler o código fonte do @adonisjs/lucid, entendi que MigrationRunner.run() captura erros silenciosamente:
async run() {
try {
await this.runUp();
} catch (error) {
this.error = error; // erro registrado, mas nunca re-lançado
}
await this.shutdown();
}
Se uma migration falha — por timeout, erro de SQL ou qualquer outro motivo — o run() termina normalmente e o bootstrap segue para o seed. O seed falha, o setup retorna erro, e a causa real nunca aparece nos logs. Isso significa que qualquer migration que não execute corretamente é engolida pelo framework.
Por que cada migration criava uma transaction no SQLite :memory:
O MigrationRunner roda cada migration dentro de uma transaction por padrão:
const client = await this.client.transaction();
Com pool: { min: 0, max: 1 } no SQLite :memory:, o pool tem uma única conexão. Cada transaction precisa adquirir essa conexão. Se a anterior não liberou a tempo (ou o better-sqlite3 tem comportamento síncrono que conflita com o pooling assíncrono do tarn.js), a próxima transaction dá timeout.
O resultado: todas as migrations que dependiam de transaction ficavam presas. E o seed — que usa a mesma conexão do pool — não enxergava as colunas adicionadas pelas migrations bloqueadas.
A solução
Duas mudanças resolveram o problema:
1. Desabilitar transactions nas migrations de teste
No config/database.ts, a conexão memory passou a incluir disableTransactions: true:
memory: {
client: 'better-sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true,
migrations: {
naturalSort: true,
paths: ['database/migrations'],
disableTransactions: true, // ← impede timeout de pool
},
},
Cada migration agora usa this.client diretamente em vez de criar uma transaction separada. Com isso, as migrations completam sem adquirir múltiplas conexões do pool.
2. Verificar o erro do migrator no bootstrap
No tests/bootstrap.ts, adicionei uma verificação após migrator.run():
await migrator.run();
if (migrator.error) {
throw migrator.error; // propaga o erro em vez de ser silenciado
}
Se qualquer migration falhar agora, o teste falha com o erro real — não com um column does not exist que mascara a causa.
O que aprendi
-
Sempre verificar o status do migrator depois de
run(). Não confie que o framework propaga erros silenciosamente — ele pode não fazer. -
disableTransactionsexiste por um motivo. Em bancos de teste em memória, transactions não são apenas desnecessárias — elas podem causar timeouts de pool. Para SQLite:memory:, desabilitar transactions elimina o gargalo. -
O teste errado te leva longe da causa real. O erro
table has no column named typesugeriu que o@columnno model estava faltando. Mas a coluna já existia no model. O problema era na infraestrutura de teste, não no código. Quando um teste falha, pergunte: "estou testando o código certo, ou estou testando o ambiente?" -
Leia o código do framework quando algo parece impossível. O
MigrationRunner.run()é 50 linhas. Entender que ele engole erros transformou 4 horas de frustração em 20 minutos de diagnóstico.
Essa experiência reforça uma verdade que todo dev backend deveria internalizar: testes de integração não são confiáveis quando a infraestrutura de teste é opaca. Se o framework esconde falhas, você precisa de camadas de verificação próprias. Não é opcional — é infraestrutura.
- Testing
- AdonisJS
- SQLite
- Backend
- NodeJS
Vinicios Clarindo
@vinicioslc
Comments