
Neste post vou compartilhar um pouco dos meus experimentos para tornar o suite de testes do Cifras mais rápido.
No atual estado do serviço eu tenho por volta de 800 testes automatizados usando Rspec 1.3 e Rails 2.3.9.
Estes testes cobrem o sistema praticamente todo e a são única forma ter a confiança necessária para trabalhar em novo recursos, correções de bugs e etc.
Este número de tests tem crescido consideravelmente desde que comecei a utilizar o Steak para testes de aceitação. Testes de aceitação possuem um papel muito importante em certificar que a funcionalidade é aceitável para o cliente em faz os testes em estado mais macro. Desta forma tenho coberto o sistema praticamente todo usando o Steak nas últimas semanas.
No entanto eu me vi em uma situação que já era perceptível mas começou a se tornar insuportável. Meu suite de testes (ainda com 800) estava demorando por volta de 4.5 minutos para ser executado. O mais estranho é que 800 testes é muito pouco para poder demorar tanto.
Arquitetura de um Saas
Cifras é um Saas que envolve pagamentos e múltiplos usuários dentro de um conta. Eu já trabalhei em vários outros Saas’s e todos que envolvem algum tipo de assinatura e vários usuários acabam tendo uma arquitetura muito parecida nos models.
Normalmente você possui os seguintes models: conta, usuário, assinatura, plano de uso, preferências e o uma associação para proprietário da conta. Além destes 5 models também existe um processo que insere alguns dados iniciais como categorias assim que uma conta é criada.
Fica claro que qualquer signup vai disparar uma criação em cadeia de uma conta com uma assinatura que possui um propietário (que é um usuário) e depois preencher as categorias e etc. Este processamento é bem rápido mas multiplicado por 800 (ou mais) acaba sendo um problema.
Eu não quero reduzir o número de models reunindo tudo no model de conta pois acredito que seria uma arquitetura bagunçada além de uma alteração que envolva tanto o banco de dados ser extremamente perigosa.
Eu não queria tocar em nada do código da aplicação apenas para fazer os testes se tornarem mais rápidos. Do contrário acredito que estaria ocorrendo um inversão das responsabilidades, fazendo os testes guiarem o desenvolvimento mais de uma forma errada.
1º passo: Logger
Comecei o trabalho de otimização analisando o log de test. Para ter certeza de onde cada query vinha e o tempo gasto, adicionei um plugin chamado query_trace.
É um plugin bastante antigo mas ainda funciona muito bem. Assim que instalei, fiz algumas anotações de todas as queries que ocorriam com frequência em meus tests e de onde elas vinham.
Mas o mais interessante que ao instalar o plugin a performance dos meus testes caiu drasticamente. A única razão para isto é que o plugin escrevia toneladas de textos no meu log, ou seja, operações que envolvem muito IO.
O que fiz foi anotar as coisas que me interessavam e remover o plugin, no entanto uma otimização que já poderia ser feita é alterar a estratégia de log do ambiente de tests para :info ao invés do padrão.
Apenas esta alteração tornou meu suite de testes quase 50 segundos mais rápido.
No arquivo config/environment/test.rb:
1 config.log_level = :info
2º passo: Evitando consultas
Analisando o resultado do query_trace não havia nenhuma consulta extremamente lenta que ocorresse com frequência. Como eu já utilizava bibliotecas como SlimScrooge, Oink e NewRelic eu já tinha encontrado estas queries e resolvido a mais tempo.
No entanto, uma coisa que muitas vezes não nos lembramos é que qualquer validação de unicidade e associação do Rails vai disparar um consulta. Sim, uma micro consulta que normalmente gasta 0.3 ms e não incomoda em nada em ambiente de produção. Mas nos tests isso pode significar muito.
Como eu utilizava apenas factories (com FactoryGirl) cada spec acabava precisando de um before ou um let do Rspec que fazia uma chamada a Factory account que consequentemente criava todas outras factories associadas e ainda executava as validações.
As criações associadas não me incomodavam ainda pois eu estava atacando apenas as validações. A melhor alternativa que encontrei para remover os 0.3ms de cada validação foi usar fixtures.
Fixtures são carregadas apenas uma vez e não passam pelas validação, então em todos os momentos que eu precisava dos dados base como conta e usuário para fazer os outros testes eu passei a usar fixtures.
Por exemplo, nos testes de categorias eu precisava criar uma conta antes para associar uma categoria a ela. Este processo rodava em um before o que tornava os testes bem mais lentos, a partir de agora tudo é previamente carregado por fixtures.
No final, terminei com uma fixture para cada model base e criei helpers para o Rspec. Algo assim:
E meus specs passaram a ficar assim:
1
2
3
A desvantagem é que novos desenvolvedores terão que entender que o account é um helper já que ele é incluído pelo spec_helper.
Outra coisa que fiz foi incluir as fixtures base como globais para evitar ter que carrega-las manualmente nos specs.
1
2 Spec::Runner.configure do |config|
config.global_fixtures = :users, :plans, :accounts, :settings ...
Em vários casos o uso de fixtures para a base do sistema eu tive ganhos de mais de 100% em velocidade. Então a partir disso toda a base do sistema (5 models) utilizam fixtures nos models associados e factories em seus próprios models.
Por exemplo, no model User eu faço os specs usando a Factory user mas a sua conta é trazida de uma fixture e vice versa com os outros models base. No caso de Transações eu carrego usuário, conta, plano, assinatura e preferência através das fixtures enquanto as transações são criadas com factories.
3º passo: Evitando chamadas remótas
Uma coisa irritante da comunidade Rails são as modinhas. Por um lado é bom, pois as coisas se atualizam MUITO rápido por outro lado fixtures se tornaram diabólicas de um dia para o outro, também ocorreu com attachment_fu por exemplo e etc. O mesmo vale para mocks.
Se você vai fazer uma integração com uma API, você DEVE usar um mock para não fazer o request, essa é a regra. Eu simplesmente descordo!
Se sua aplicação depende da API para existir, eu considero totalmente errado fazer um mock disso pois você nunca saberá se por algum motivo a API mudou ou parou de funcionar, ou mudou de endereço.
Apesar de óbvio que uma API deve ter versões e não mudar dessa forma, nem sempre isso ocorre, ainda mais as API’s brasileiras.
Por esta razão os testes do Cifras executavam requests para o sandbox do MOIP, o que fazia eu ter a segurança que o meu código continuava funcionando com a API deles. Eu NÃO iria fazer um mock dessa parte e ponto final.
Isso causava uma demora de mais ou menos 1 min nos testes. Mas era um preço que eu preferia pagar.
Com uma dica do meu amigo Jeffry Degrande conheci uma ferramenta que resolve isso. O ephemeral_response faz o request para a API e armazena um cache com o retorno por um prazo pré-determinado. Defini que este prazo no Cifras seria de um dia.
Com este passo reduzi mais um minuto do meu suite.
4º passo: Trabalho de formiga
O último passo seria de fato passar spec por spec e olhar o que poderia ser otimizado. Em alguns pontos stubs faziam mais sentido, outros apenas um Factory.build ao invés de create tornava o test mais rápido.
Outras duas técnicas muito boas é utilizar Factory.attributes_for :meu_model e Factory.stub :meu_model.
Este passo não reduziu muito o tempo pois eu já utilizava desta forma, mas em alguns pontos eu tinha deixado passar então tive uma pequena melhoria.
Conclusão
O resultado final foi que de 5min eu consegui reduzir para 1.5 min. Ainda existe espaço para otimizações, mas por enquanto isso deixo de ser prioridade e será melhorado usando aos poucos seguindo The Boy Scout Rule.
A minha conclusão é que fixtures são excelente para alguns casos e factories também, então não existe nenhum motivo para este ódio contra fixtures. Você pode usar tanto fixtures quanto factories no mesmo projeto e tirar o melhor dos dois mundos.
Outra dica é quanto ao log e o ephemeral_response e sempre ficar atento para usar Factory.build e Factory.stub quando possível.
Se você tiver alguma outra dica, compartilhe nos comentários.




