O teste que lê o código-fonte: guardar uma regra em vez de uma função
Um teste normal dá uma entrada e espera uma saída. Serve para quase tudo, e tem um limite claro: só apanha aquilo que alguém se lembrou de verificar.
Há outro género, que se vê pouco escrito e que resolve um problema diferente — guardar uma regra que não vive dentro de nenhuma função. Uma decisão de arquitetura não se parte quando se mexe no que já existe; parte-se quando se acrescenta a coisa seguinte, e é precisamente aí que ninguém está a pensar nela.
1. O caso que me levou a escrever isto
Neste site, uma Server Action é um endpoint público: quem souber o identificador dela chama-a. O proxy protege as páginas de /admin e não defende as ações. Cada uma tem de verificar a sessão por si, e todas passam por um invólucro:
export const guardarArtigo = comSessao(async (dados) => {
// ...
});A regra é «nenhuma ação escapa ao invólucro». Um teste comum não a guarda: teria de haver um teste por ação, e escrever o teste da ação nova é exatamente aquilo de que quem se esquece do invólucro também se esquece.
O teste que guarda isto não chama código nenhum. Lê o ficheiro como texto:
const fonte = readFileSync(ficheiro, 'utf8');
test('todas as ações de administração exigem sessão', () => {
const exportadas = [...fonte.matchAll(/^export const (\w+) = (\w+)\(/gm)]
.map((m) => ({ nome: m[1], invólucro: m[2] }));
assert.ok(exportadas.length > 0, 'não encontrei ações exportadas');
const desprotegidas = exportadas.filter((a) => a.invólucro !== 'comSessao');
assert.deepEqual(desprotegidas.map((a) => a.nome), []);
});Acrescentar uma ação sem o invólucro faz a bateria ficar vermelha, com o nome dela na mensagem. Ninguém teve de se lembrar de nada.
2. A parte que quase toda a gente esquece
Repare naquela linha do meio:
assert.ok(exportadas.length > 0, 'não encontrei ações exportadas');É a mais importante das três. Um teste que procura padrões num ficheiro e não encontra nenhum passa — e passa a sorrir, para sempre, sem guardar coisa nenhuma. Basta alguém mudar o estilo das declarações, renomear o ficheiro, ou partir o caminho.
Sem essa verificação, o teste deixa de proteger em silêncio no dia em que mais faz falta. É o modo de falhar próprio desta técnica, e não é opcional.
3. Fechar a porta das traseiras
O primeiro teste só olha para export const x = .... Uma ação declarada de outra maneira escapava-lhe sem levantar ruído:
test('nenhuma ação é exportada como função solta', () => {
const soltas = [...fonte.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)/gm)]
.map((m) => m[1]);
assert.deepEqual(soltas, [], 'declare as ações como export const x = comSessao(...)');
});Um teste que lê o código-fonte tem sempre uma forma de ser contornado sem má intenção nenhuma — alguém escreve noutro estilo e o teste nem dá por isso. Vale a pena perguntar, ao escrevê-lo, «e se isto for declarado de outra maneira?», e fechar essa porta também.
4. Duas listas que têm de andar a par
O outro uso, mais frequente: dois ficheiros que estão separados por uma razão e que, por estarem separados, se desencontram com facilidade.
A barra do editor deste site oferece uma lista de linguagens para os blocos de código. Essa lista vive à parte do ficheiro que regista as gramáticas do realce — porque a barra é um componente de cliente, e importar dali arrastaria a biblioteca inteira para o browser de quem escreve.
test('cada linguagem oferecida tem gramática registada', () => {
for (const { valor } of LINGUAGENS_DISPONIVEIS) {
assert.ok(linguagemConhecida(valor), `${valor} está na lista mas não está registada`);
}
});Sem isto, escolher uma linguagem sem gramática dava um bloco de código sem cor nenhuma — sem erro, sem aviso, e sem ninguém perceber porquê. Aconteceu-me duas vezes antes de o teste existir, com o dockerfile e com o nginx.
5. Quando é que vale a pena
Três condições, e convém estarem as três:
A regra não cabe num sítio só. Se coubesse, punha-se lá uma verificação em execução e acabava.
A regressão entra com o que vier a seguir, não com o que já está escrito. É o que distingue isto de um teste normal.
Falhar é silencioso e caro. Uma ação sem sessão não dá erro nenhum — funciona lindamente, para toda a gente.
6. O que isto custa
Ler código como texto é frágil, e não vale a pena fingir o contrário. Uma expressão regular sobre declarações parte-se com uma mudança de estilo inofensiva.
A meu ver isso é meio defeito e meio virtude: obriga a olhar para a regra outra vez, de propósito, em vez de a mudar por acidente. Mas só funciona se o teste explicar porque existe — um teste destes sem comentário é, daqui a um ano, um obstáculo sem motivo aparente, e alguém o apaga.
Para regras que se repetem em muitos sítios há ferramentas próprias: regras de ESLint escritas de raiz, ou verificadores de dependências entre pastas. Para uma regra só, um teste de vinte linhas fica mais barato do que um plugin — e lê-se melhor.
O resumo
Nem tudo o que se quer garantir é comportamento. Quando a garantia é «isto tem de ser sempre feito assim», e quando quebrá-la não dá erro nenhum, um teste que abre o ficheiro e olha para ele resolve. Desde que se lembre da linha que verifica que ele encontrou alguma coisa.