Melhorando Seus Testes em Ruby com Spork e Guard

Com o decorrer de qualquer projeto a bateria de testes vão crescendo com os testes de unidade, funcionais, integração, aceitação e afins. Além disso criamos diversas configurações das quais carregam bibliotecas auxiliares, inclusive o Rails. Com isso começamos a perceber uma lentidão na hora de rodar os testes. Dado esse problema veremos como melhor nossos testes em performance e facilidade de execução.

Objetivo

Tornar os testes mais rápidos e mais fáceis de serem executados utilizando o Spork e o Guard

Spork

Normalmente em nossas specs, carregamos o famoso spec_helper.rb, pois é nele que ficam declaradas algumas bibliotecas necessárias para rodar os testes como, por exemplo, o Capybara, Database Cleaner, RSpec e o Rails. E todo este carregamento ocorre cada vez que rodamos os testes, o que pode ser muito demorado dependendo do tamanho do projeto.

O Spork basicamente faz todo este carregamento uma única vez em seu próprio servidor, o Distributed object system for Ruby, assim, quando os testes forem executados, tudo já estará carregado poupando um bom tempo.

Para manipular o Spork iremos utilizar a gem guard-spork que faz uma abstração pra gente:

group :development do
  gem 'guard-spork', '~> 1.4.0'
end

E então podemos iniciar a sua configuração:

bundle install
guard init spork

Um arquivo chamado Guardfile, nascido da combinação Guard + Spork, será criado na raiz do projeto e explicado logo adiante:

# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do
  watch('config/application.rb')
  watch('config/environment.rb')
  watch('config/environments/test.rb')
  watch(%r{^config/initializers/.+\.rb$})
  watch('Gemfile')
  watch('Gemfile.lock')
  watch('spec/spec_helper.rb') { :rspec }
  watch('test/test_helper.rb') { :test_unit }
  watch(%r{features/support/}) { :cucumber }
end

Guard

O Guard, como o nome mesmo já sugere, é um camarada que fica vigiando alterações nos arquivos do SO que dispara ações onde podemos executar alguns eventos. No arquivo Guardfile o Guard “assistindo” alguns arquivos como:

watch('config/application.rb')

Como esses watcher estão dentro do block guard 'spork', quando algum dos arquivos descrito for alterado, o Spork será recarregado. Agora que já sabemos um pouco do Spork e do Guard, vamos personalizar o nosso Guardfile:

guard :spork, wait: 120, test_unit: false, cucumber: false, rspec_env: { 'RAILS_ENV' => 'test' } do
  watch(%r(^config/initializers/.+\.rb$))
  watch('config/application.rb')
  watch('config/environments/test.rb')
  watch('Gemfile.lock')
  watch('spec/spec_helper.rb')
end

No bloco :spork adicionamos algumas configurações:

wait: tempo de espera até o servidor subir. Para aplicações mais pesadas isso é importante; test_unit: não iremos utilizá-lo, pois vamos usar o RSpec; cucumber: não iremos gerenciar os testes de Cucumber; rspec_env: dissemos ao Spork que durante os testes de RSpec o RAILS_ENV terá o valor “test”. E fique atento a isto, pois se durante os testes de Cucumber, por exemplo, fizéssemos:

ENV['RAILS_ENV'] ||= 'cucumber'

E na configuração do Spork colocássemos:

cucumber_env: { 'RAILS_ENV' => 'test' }

O Guard não seria ativado, pois ele estaria esperando o valor test na variável de ambiente “RAILS_ENV”, sendo que estaríamos setando cucumber durante os testes.

O reload do DRb, só será feito quando a alteração de um arquivo puder afetar o comportamento do Spork. Agora precisamos automatizar o RSpec:

Primeiramente vamos adicionar a gem guard-rspec no Gemfile:

group :development do
  gem 'guard-rspec', '~> 2.3.1'
end

E adicionar o bloco de configuração do RSpec no Guardfile:

guard :rspec, cli: '--drb --color --format doc', all_on_start: false, all_after_pass: false do
  watch(%r(^spec/.+_spec\.rb$))
  watch(%r(^app/models/(.+)\.rb$))  { |m| "spec/models/#{m[1]}" }
  watch(%r(^app/models/(.+)\.rb$))  { |m| "spec/features/#{m[1]}" }
  watch('spec/spec_helper.rb')      { 'spec' }
end

Neste block utilizamos a propriedade cli para dizer o que será automaticamente executado na linha de comando durante a execução do rspec:

--drb: dize que iremos rodar os testes no Spork DRb server; --color: opção para colorir o output dos testes; --format doc: para deixar o label dos testes em formato de documento; all_on_start: desligamos para não ser rodado todos os testes de RSpec assim que o Spork subir; all_after_pass: desligamos para não rodar todos os testes após um simples testes passar.

E então fazermos alguns de-para de execução:

watch(%r(^spec/.+_spec\.rb$))

Utilizamos regex para dizer que se qualquer arquivo que termine com _spec.rb que esteja dentro da pasta spec for alterado o comando rspec será executado para este próprio arquivo. Alterações no arquivo spec/article/validations_spec.rb executaria o comando:

rspec spec/article/validations_spec.rb --drb --color --format doc

Podemos também rodar alguma classe de teste específica quando algum arquivo for alterado, não ficando preso na execução da própria classe de teste alterada:

watch(%r(^app/models/(.+)\.rb$)) { |m| "spec/features/#{m[1]}" }

Aqui qualquer arquivo alterado que termine com .rb e esteja dentro da pasta app/models irá rodar sua respectiva specs. O nome do arquivo alterado é capturado pelo grupo (.+) que é passado para o bloco na variável m, logo “app/models/(article).rb” executará todos os arquivo da pasta “spec/models/(article)”. (:

Mas se quisermos rodar toda a bateria de teste podemos fazer assim:

watch('spec/spec_helper.rb') { 'spec' }

DRb

Com todas as regras do Guard configuradas, vamos configurar o que será carregado no servidor DRb uma única vez (prefork) e o que terá que ser executado antes de cada bateria de teste (each_run). Estas duas características são definidas pelo conteúdo dos em parenteses. Na maioria das vezes podemos colocar todo o conteúdo spec_helper no block prefork para ser executado uma única vez.

Vamos utilizar o Bootstrap do Spork para gerar as devidas configurações para gente:

spork --bootstrap

Agora vamos editar o spec_helper.rb e passar todo conteúdo exemplo que será carregado uma única vez para dentro do bloco prefork:

require 'rubygems'
require 'spork'

Spork.prefork do
  ENV['RAILS_ENV'] ||= 'test'

  require File.expand_path('../../config/environment', __FILE__)
  require 'capybara/rails'
  require 'database_cleaner'
  require 'rspec/rails'

  Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

  RSpec.configure do |config|
    config.after { DatabaseCleaner.clean }
  end
end

Spork.each_run do
end

Os dois primeiros requires deve ficar fora dos blocos e estamos com o Spork configurado e pronto para ser testado, mas antes vamos ver qual o tempo atual dos nossos testes:

time rspec

# Finished in 47.03 seconds
# 569 examples, 0 failures, 54 pending

# real  0m51.477s
# user 0m44.510s
# sys  0m2.912s

Agora vamos subir o Spork executando:

spork

# Using RSpec
# Preloading Rails environment
# Loading Spork.prefork block...
# Spork is ready and listening on 8989!

Sim, a aba do seu terminal ficará presa e o Spork ficará rodando na porta 8989. Repare que é digo que o block prefork foi carregado com sucesso. Então iremos abrir outra aba e executar novamente os testes, mas destava vez aproveitando o load do DRb utilizando o parâmetro --drb:

time rspec spec --drb

# Finished in 46.09 seconds
# 569 examples, 0 failures, 54 pending

# real  0m50.093s
# user 0m1.387s
# sys 0m0.187s

Veja que o tempo real da execução dos testes deste blog github.com/wbotelhos/wbotelhos-com-br não teve tanta diferença, pois não carrego tanta coisa no boot do mesmo, mas mesmo assim vemos a diferença do uso do CPU no processo do usuário e do kernel abaixando.

Em um dos projetos que compoe o CMS do R7.com que também depende de carregar vários outros pequenos projetos, o tempo caiu de 1:47 para 0:24 em uma bateria de quase 500 teste, ou seja, mais de um minuto era apenas para carregar as dependências. E esperar tudo isso, mesmo que rodemos uma única classe de teste é um grande desperdício de tempo.

Auto Teste

Além de toda essa otimização podemos ter o conforto de deixar o Guard rodar os testes de acordo com a nossa configuração no Guardfile. Precisamos de uma API para notificar o Guard que algum arquivo foi alterado e ele possa executar os eventos, e esta API varia de SO para SO:

OS X: rb-fsevent Linux: rb-inotify Windows: rb-fchange

Utilizo Linux no trabalho e OS X em casa, mas se você alterna de ambiente muito dinâmicamente, você pode declarar as gems dos SOs que você utiliza que a apropriada será carregada corretamente.

Faça isso caso as coisas no seu serviço aconteçam muito rápidas. INTERNA, piada (risos)

Linux

group :development do
  gem 'rb-inotify', '~> 0.9.0', require: false
end

OS X

group :development do
  gem 'rb-fsevent', '~> 0.9.3', require: false
end

Agora instale as gems:

bundle install

Increasing the amount of inotify watchers

O inotify tem um limite de arquivos que o mesmo consegue monitorar, porém podemos aumentar esse limite com o seguinte comando:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

Guard console

Agora vamos subir o guard para ativar os nossos observadores:

guard

# 23:31:13 - INFO - Guard uses TerminalTitle to send notifications.
# 23:31:13 - INFO - Starting Spork for RSpec
# Using RSpec
# Preloading Rails environment
# Loading Spork.prefork block...
# Spork is ready and listening on 8989!
# 23:31:16 - INFO - Spork server for RSpec successfully started

# 23:31:16 - INFO - Guard::RSpec is running
# 23:31:16 - INFO - Guard is now watching at '/Users/wbotelhos/workspace/wbotelhos-com-br'
# [1] guard(main)>

Veja que o Guard subiu o Spork e já esta observando o projeto. Logo se alterarmos alguma classe o Guardfile será lido e de acordo com o de-para que fizemos os testes serão executados.

O console disponibilizado possui vários comandos e aceita algumas letras seguida de enter para executar comandos:

a: (all) Roda todos os testes. r: (reload) Faz o reload do Spork. p: (pause) Pausa e despausa o listening. q ou e: (quit / exit) Finaliza o console. s: (status) Mostra toda a configuração do Guardfile.

Para conhecer outros comandos execute:

h: Help.

Cache

Um caso muito comum de acontecer é a não execução do Guard quando uma classe é alterada. Depois de algumas pesquisas o desenvolvedor constata que é problema de cache. Para resolver podemos fazer o reload das classes desejadas no bloco each_run:

Dir[Rails.root + 'app/**/*.rb'].each { |file| load file }

E pra quem utiliza a gem FactoryGirl, chamar o método reload para recarregar as factories:

FactoryGirl.reload

Outro problema também é o cache setado no arquivo config/environments/test.rb. Quando o Spork sobe o DRb ele seta uma variável de ambiente chamada DRB com o valor 'true', baseado nesse valor ativamos ou não o cache no ambiente de teste:

config.cache_classes = ENV['DRB'] != 'true'

Assim só iremos utilizar o cache se o Spork não estiver rodando. (:

Isolamento de teste

A primeira coisa que você irá perceber e se assustar é que ao alterarmos apenas uma linha de código todo o arquivo de teste mapeado para tal classe será executado. E se tivermos umas quantidade grande de testes, todos serão executados, mas as vezes queremos executar apenas um pequeno contexto. Para isso podemos utilizar a opção de filtro onde focamos o RSpec apenas em alguns testes. No spec_helper.rb adicione o seguinte block de código:

RSpec.configure do |config|
  config.treat_symbols_as_metadata_keys_with_true_values = true
  config.filter_run focus: true
  config.run_all_when_everything_filtered = true
end

No primeiro comando habilitamos o uso de meta-dados nos blocos de testes, ou seja, podemos adicionar parâmetros que poderão fazer parte do bloco de teste:

it 'use metadata', some: 'metadata' do
  ...
end

Com isso habilitado podemos fazer um filtro de qual teste o Guard irá rodar baseado em um meta-dado de nossa escolha, que no caso será o símbolo :focus, ficando assim:

it 'run isolated', :focus do
  ...
end

O próprio símbole é considerado true, logo o Guard irá rodar apenas os testes que tiverem este meta-dado. E se não tivermos nenhum :focus na classe de teste? Bem, ai nenhum teste será executado, e é ai que entra a última configuração que diz que se não for encontrado nenhum meta-dado, todos os testes serão executados.

Notification

Repare que na primeira linha do output do comando guard é dito “Guard uses TerminalTitle to send notifications.”, isso porque não temos nenhum API configurada para notificar as ações do Guard, logo isto será apenas escrito na aba do terminal, o que já é legal. Para termos uma pop up bonitinha, vamos utilizar a gem ruby_gntp:

group :development do
  gem 'ruby_gntp', '~> 0.3.4'
end

Temos diversas outras gems para notificação, cada uma com suas restrições e features. Eu decidi utilizar a citada acima, por ser suportada tanto no Linux quando no OS X e utilizar o protocólo do Growl que curto bastante. Para o Linux ele é grátis, para o Mac custa $3.99.

Temos outras gems para o OS X, mas em sua maioria depende do Growl:

growl: A gem original do Growl que além de ser paga necessita de instalação do growlnotify, um pequeno .dpkg; terminal-notifier-guard: Não depende do Growl, mas só é compatível com o OS X 10.8. Se você não pagou o Growl, terá que pagar o SO. growl_notify: Usa o Growl e não terá suporte de notificação utilizando o Spork, ou seja, néca né?.

O suporte para Windows vou ignorar achando que você esta brincando e é isso ai, agora é partir para os testes. (;