Otimismo de Recursos.
Otimismo de Recursos é quando um teste presume que um recurso externo (um arquivo, diretório, tabela de banco de dados, variável de ambiente ou endpoint de rede) já existe e está em um estado conhecido, em vez de provisioná-lo e verificá-lo, fazendo o teste passar ou falhar de forma não determinística.
##Signs and Symptoms
Um teste toca algo fora de si mesmo — um arquivo, caminho temporário, diretório, linha de banco de dados, variável de ambiente ou endpoint remoto — e simplesmente confia que ele já está lá e com o formato correto. Não há etapa de configuração que crie o recurso nem verificação de que ele existe antes de ser usado.
Sinais reveladores:
- Um caminho embutido no código é lido ou escrito sem um
existsSync/mkdir/writeFileprévio: por exemplo,fs.readFileSync('/tmp/app/config.json'). - O teste passa na sua máquina ou na primeira execução, depois falha em um checkout limpo, na CI, em um diretório temporário de outro SO, ou quando a suíte roda em ordem diferente ou em paralelo.
- O recurso de que ele precisa é, na verdade, criado por outro teste (ou por uma etapa manual/exclusiva de dev), então o teste só funciona como efeito colateral da ordem de execução.
- Ele abre um arquivo/conexão e faz asserções sobre o resultado sem nunca verificar que o recurso estava presente — um recurso ausente lança uma exceção antes da asserção real, ou silenciosamente produz dados vazios que mesmo assim "passam".
test('parses the config file', () => {
// otimista: presume que /tmp/app/config.json já existe e está bem formado
const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
A heurística canônica de detecção (testsmells.org / tsDetect): um teste usa um recurso do tipo File sem antes chamar uma verificação de existência/validade como exists(), isFile() ou notExists().
##Reasons for the Problem
Por que acontece
- O recurso estava presente enquanto o teste era escrito (um arquivo de fixture no repositório, um banco de dev populado, um arquivo temporário deixado por uma etapa anterior), então o autor nunca sente a ausência dele.
- É simplesmente menos código apontar para um caminho ou tabela existente do que alocar e popular um na configuração, e depois desfazê-lo.
- Copiar e colar de outro teste que já dependia de estado compartilhado e ambiente.
Por que prejudica
- Não determinismo / instabilidade (flakiness). O resultado depende do estado do ambiente, não do código sob teste. van Deursen et al. descrevem exatamente isto: testes que "funcionam bem em um momento e falham miseravelmente em outro." Runners de CI limpos, checkouts novos, workers paralelos e diferentes locais temporários do SO são onde isso causa dor.
- Falsa confiança. Um teste verde pode estar passando por causa de estado remanescente de uma execução anterior ou de um teste anterior, não porque o código atual está correto — ou um recurso ausente lança uma exceção cedo e a asserção significativa nunca é executada.
- Acoplamento oculto e dependência de ordem. Quando um teste cria aquilo que outro consome, a suíte tem um contrato de ordenação invisível que quebra ao embaralhar ou fragmentar (sharding) os testes.
- Difícil de reproduzir e manter. As falhas não podem ser reproduzidas localmente porque dependem de estado de ambiente específico da máquina, então a depuração é lenta e o teste corrói a confiança.
##Treatment
Faça cada teste possuir e controlar todo recurso que ele toca, e nunca presuma estado de ambiente.
- Provisione na configuração, limpe no teardown. Use Setup External Resource — aloque e inicialize arquivos, diretórios, tabelas de banco e conexões em
beforeEach/beforeAlle libere-os emafterEach/afterAllpara que a próxima execução comece limpa. - Crie, não presuma. Escreva o arquivo, popule a tabela ou inicie o servidor de stub na configuração. Se você realmente precisar consumir um recurso pré-existente, verifique primeiro que ele existe para que um recurso ausente falhe de forma evidente com uma mensagem clara, em vez de corromper a asserção real.
- Isole por teste. Use um diretório temporário único (
fs.mkdtemp(os.tmpdir() + …)) ou um schema/namespace novo por teste, em vez de um caminho compartilhado embutido no código, para que execuções paralelas e reexecuções não colidam. - Melhor ainda, remova a dependência. Substitua o recurso real por um mock ou fake em memória (
fsmockado, banco em memória, HTTP com stub) para que o teste seja totalmente autocontido e determinístico — a correção que o testsmells.org recomenda.
Antes → depois:
// antes — Otimismo de Recursos
test('parses the config file', () => {
const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
// depois — o teste possui e verifica seu recurso
import { mkdtemp, writeFile, rm, readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
let dir;
beforeEach(async () => {
dir = await mkdtemp(path.join(os.tmpdir(), 'cfg-'));
await writeFile(path.join(dir, 'config.json'), JSON.stringify({ port: 8080 }));
});
afterEach(() => rm(dir, { recursive: true, force: true }));
test('parses the config file', async () => {
const raw = await readFile(path.join(dir, 'config.json'), 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
##Detected by
- tsDetect Resource Optimism — Otimismo de Recursos