javascript-unit-testing-performance
Esta página foi traduzida por PageTurner AI (beta). Não é oficialmente endossada pelo projeto. Encontrou um erro? Reportar problema →
O Jest executa milhares de testes no Facebook constantemente, seja por integração contínua ou acionado manualmente por engenheiros durante o desenvolvimento. Isso funcionou bem por anos, mesmo quando as pessoas que trabalhavam no Jest migraram para outros projetos dentro do Facebook.
Porém, conforme os engenheiros adicionavam cada vez mais testes, percebemos que o desempenho do Jest não escalaria. Além disso, no último ano o ecossistema JavaScript mudou dramaticamente com a introdução de elementos como npm3 e Babel, que não havíamos antecipado. Formamos uma nova equipe do Jest para resolver todas essas questões e compartilharemos nosso progresso e planos neste blog a partir de agora.
O Jest é um pouco diferente da maioria dos executores de testes. Nós o projetamos para funcionar bem no contexto da infraestrutura do Facebook:
-
Monorepo No Facebook, temos um enorme monorepo que contém todo nosso código JavaScript. Há muitas razões pelas quais essa abordagem é vantajosa para nós, e há uma palestra excelente de um engenheiro do Google que destaca todos os benefícios e desvantagens dos monorepos.
-
Sandboxing Outra funcionalidade do Jest importante para o Facebook é como ele virtualiza o ambiente de teste e encapsula o
requirepara isolar a execução do código e testes individuais. Estamos até trabalhando para tornar o Jest mais modular e aproveitar essa funcionalidade em outros casos de uso não relacionados a testes. -
providesModule Se você já analisou nossos projetos JavaScript de código aberto, deve ter notado que usamos um cabeçalho
@providesModulepara atribuir IDs globalmente únicos aos módulos. Isso requer ferramentas personalizadas, mas nos permite referenciar módulos sem caminhos relativos, o que nos ajudou a avançar incrivelmente rápido, escalou bem conforme nossa organização de engenharia cresceu e fomentou o compartilhamento de código em toda a empresa. Veja o RelayContainer para um exemplo prático. Uma desvantagem dessa abordagem, porém, é que somos forçados a ler e analisar toda nossa base de código JavaScript para resolver uma única instrução require. Isso seria proibitivamente caro sem cache extensivo, especialmente para processos de curta duração como o Jest.
Devido a essas restrições únicas, o Jest talvez nunca seja tão rápido quanto outros executores de testes ao rodar toda nossa suíte de testes. No entanto, engenheiros raramente precisam executar o Jest em toda a suíte. Movidos pela análise estática do projeto node-haste, definimos como modo padrão de execução no Facebook jest --onlyChanged ou jest -o. Nesse modo, construímos um gráfico de dependência reversa para encontrar apenas os testes afetados que precisam ser executados com base nos módulos alterados.
Agendamento Otimizado de Execução de Testes
Na maioria das vezes, nossa análise estática determina que mais de um teste precisa ser executado. O número de testes afetados pode variar de alguns a milhares. Para acelerar esse processo, o Jest paraleliza a execução de testes entre workers. Isso é ótimo porque a maior parte do desenvolvimento no Facebook ocorre em servidores remotos com muitos núcleos de CPU.
Recentemente percebemos que o Jest frequentemente parecia travado com "Waiting for 3 tests" por até um minuto no final da execução. Descobrimos que havia alguns testes muito lentos em nossa base de código que dominavam o tempo de execução. Embora tenhamos acelerado significativamente esses testes individuais, também alteramos como o Jest agenda as execuções. Anteriormente, o agendamento era baseado na travessia do sistema de arquivos, que era bastante aleatória. Aqui está um exemplo de 11 testes em blocos cinza distribuídos em dois workers, onde o tamanho do bloco representa o tempo de execução do teste:
Estávamos executando aleatoriamente uma mistura de testes rápidos e lentos, e um de nossos testes mais lentos acabava rodando quando quase todos os outros já haviam terminado, deixando o segundo worker ocioso durante esse período.
Implementamos uma alteração para agendar testes com base no tamanho do arquivo, que geralmente é um bom indicador do tempo de execução do teste. Um teste com milhares de linhas provavelmente levará mais tempo que um com 15 linhas. Embora isso tenha acelerado a execução geral em cerca de 10%, descobrimos uma heurística melhor: agora o Jest armazena o tempo de execução de cada teste em cache e, em execuções subsequentes, agenda os testes mais lentos primeiro. Isso melhorou o tempo total de execução em aproximadamente 20%.
Aqui está um exemplo da mesma execução de testes com o novo agendamento:
Como executamos os testes lentos primeiro, o Jest pode parecer demorar para iniciar – só exibimos resultados após o primeiro teste ser concluído. No futuro, planejamos executar primeiro os testes que falharam anteriormente, pois fornecer essa informação rapidamente aos desenvolvedores é o mais importante.
Requisições Inline e Mocking Preguiçoso
Se você já escreveu testes usando Jasmine, eles provavelmente se parecem com isto:
const sum = require('sum');
describe('sum', () => {
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
});
Uma coisa especial que fazemos no Jest é redefinir todo o registro de módulos após cada teste (chamada para it) para garantir que os testes não dependam uns dos outros. Antes do Jest, os testes individuais dependiam uns dos outros e o estado interno do módulo frequentemente vazava entre eles. Conforme os engenheiros removiam, reordenavam ou refatoravam os testes, alguns deles começavam a falhar, dificultando que as pessoas entendessem o que estava acontecendo.
Cada teste no Jest recebe uma nova cópia fresca de todos os módulos, incluindo novas versões de todas as dependências simuladas que levam muito tempo para serem geradas por teste. Um efeito colateral disso é que tínhamos que chamar require manualmente antes de cada teste, assim:
let sum;
describe('sum', () => {
beforeEach(() => {
sum = require('sum');
});
it('works', () => {
expect(sum(5, 4)).toBe(9);
});
it('works too', () => {
// This copy of sum is not the same as in the previous call to `it`.
expect(sum(2, 3)).toBe(5);
});
});
Construímos uma transformação do babel chamada inline-requires que remove as declarações de require de nível superior e as coloca em linha no código. Por exemplo, a linha const sum = require('sum'); será removida do código, mas cada uso de sum no arquivo será substituído por require('sum'). Com essa transformação, podemos escrever testes como você esperaria no Jasmine e o código é transformado nisto:
describe('sum', () => {
it('works', () => {
expect(require('sum')(5, 4)).toBe(9);
});
});
Um ótimo efeito colateral das requires inline é que nós apenas requeremos os módulos que realmente usamos dentro do próprio teste, em vez de todos os módulos que usamos em todo o arquivo.
Um ótimo efeito colateral das requisições inline é que só carregamos os módulos realmente usados no teste, não todos os módulos do arquivo.
Conseguimos atualizar todos os testes usando um codemod em pouco tempo – foi uma alteração de código simples de 50.000 linhas. As requisições inline e o mocking preguiçoso melhoraram o tempo de execução dos testes em 50%.
O plugin babel de inline-require não é útil apenas para o Jest, mas também para JavaScript normal. Ele foi enviado por Bhuwan a todos os usuários do facebook.com há apenas uma semana e melhorou significativamente o tempo de inicialização.
Por enquanto, se você quiser usar essa transformação no Jest, terá que adicioná-la manualmente à sua configuração do Babel. Estamos trabalhando em formas de tornar isso mais fácil de optar.
Por enquanto, para usar essa transformação no Jest, você precisará adicioná-la manualmente à sua configuração do Babel. Estamos trabalhando em formas de simplificar essa adoção.
A versão open source do Jest costumava ser um fork da nossa versão interna, e sincronizávamos apenas a cada alguns meses. Era um processo manual doloroso que exigia ajustes em muitos testes. Recentemente, atualizamos o Jest para ter paridade em todas as plataformas (iOS, Android e web) e habilitamos nosso processo de sincronização contínua. Agora, toda alteração no Jest open source é testada contra nossos testes internos, e há uma única versão consistente em todos os lugares.
A primeira funcionalidade que aproveitamos após a unificação foi o cache de pré-processamento. Se você usa Babel com Jest, o Jest precisa pré-processar cada arquivo JavaScript antes de executá-lo. Construímos uma camada de cache para que cada arquivo, quando inalterado, precise ser transformado apenas uma vez. Após unificar o Jest, conseguimos corrigir facilmente a implementação em código aberto e implantá-la no Facebook. Isso resultou em mais 50% de ganho de desempenho. Como o cache só funciona na segunda execução, o tempo de inicialização do Jest permaneceu inalterado.
Também percebemos que estávamos realizando muitas operações de caminho ao resolver requires relativos. Como o registro de módulos é reiniciado para cada teste, havia milhares de chamadas que poderiam ser memorizadas. Uma grande otimização foi adicionar muito mais cache, não apenas em torno de um único teste, mas também entre arquivos de teste. Anteriormente, gerávamos metadados de módulo para o recurso de automocking uma vez por arquivo de teste. Porém, o objeto que um módulo exporta nunca muda, então agora compartilhamos esse código entre arquivos de teste. Infelizmente, como JavaScript e Node.js não têm memória compartilhada, precisamos fazer todo esse trabalho pelo menos uma vez por processo worker.
Questionar Tudo
Ao tentar melhorar o desempenho, é importante também analisar os sistemas acima e abaixo do seu. No caso do Jest, por exemplo, coisas como Node.js e os próprios arquivos de teste. Uma das primeiras ações foi atualizar o Node.js no Facebook da antiga versão 0.10 para iojs e posteriormente para Node 4. A nova versão do V8 ajudou a melhorar o desempenho e foi fácil de atualizar.
Percebemos que o módulo path no Node.js é lento ao fazer milhares de operações de caminho, o que foi corrigido no Node 5.7. Até eliminarmos o suporte para Node 4 internamente no Facebook, forneceremos nossa própria versão do módulo fastpath.
Em seguida, começamos a questionar o desatualizado node-haste. Como mencionado, todo o projeto precisa ser analisado para cabeçalhos @providesModule para construir um grafo de dependências. Quando esse sistema foi criado, node_modules não existia e nosso rastreador de arquivos não os excluía corretamente.
Em versões anteriores, o Jest lia todos os arquivos em node_modules – o que contribuía para sua lentidão na inicialização. Quando retomamos o Jest, substituímos todo o projeto por uma nova implementação baseada no empacotador do react-native. O tempo de inicialização do Jest agora é inferior a um segundo, mesmo em grandes projetos. A equipe do react-native, especialmente David, Amjad e Martin, fez um trabalho excepcional.
Somando Tudo
Muitas dessas alterações melhoraram o tempo de teste em 10% ou até 50%. Começamos com cerca de 10 minutos para todos os testes, e sem essas melhorias provavelmente estaríamos em 20 minutos agora. Após as otimizações, porém, leva consistentemente cerca de 1 minuto e 35 segundos!
Mais importante: adicionar novos testes faz o tempo total crescer muito lentamente. Engenheiros podem escrever e executar mais testes sem sentir os custos.
Com o recente lançamento do Jest 0.9 e melhorias da integração node-haste2, o tempo da suíte de testes do Relay caiu de 60 para 25 segundos, e a do react-native agora termina em menos de dez segundos em um MacBook Pro 13”.
Estamos muito satisfeitos com os ganhos obtidos e continuaremos trabalhando para melhorar o Jest. Se quiser contribuir, entre em contato via GitHub, Discord ou Facebook :)
