Criando reporters personalizados no Minitest
Leia em 4 minutos
O Minitest está tendo um grande momentum ultimamente. Eu estou usando em alguns projetos e, vindo do RSpec, sinto muita falta de seu relatório de execução extrememante detalhado. O Minitest tem alguns reporters disponibilizados no Rubygems, mas nenhum chega perto do RSpec.
Eu decidi, então, que era hora de aprender um pouco mais sobre o Minitest e criei um reporter muito parecido com o do RSpec, que ficou com essa cara:
Perceba como ele até exibe o comando que você deve executar quando um teste falha, assim como no RSpec!
Saiba que estender o Minitest é bastante simples, e você verá como fazer isso neste artigo.
Entendendo o sistema de plugins do Minitest
O sistema de plugins do Minitest usa descoberta de arquivos. Ele carrega todos os arquivos presentes no seu $LOAD_PATH
que estejam disponíveis em minitest/*_plugin.rb
. Então, se tivermos uma gem chamada minitest-utils
, podemos criar um arquivo em lib/minitest/utils_plugin.rb
, e o Minitest irá carregá-lo. Este arquivo pode implementar dois métodos no módulo Minitest
, como você pode ver à seguir:
module Minitest
def self.plugin_utils_init(options)
# ...
end
def self.plugin_utils_options(opts, options)
# ...
end
end
Os métodos devem seguir a nomenclatura Minitest.plugin_*_init
e Minitest.plugin_*_options
. Vamos ver um pouco sobre a responsabilidade de cada um deles.
Se você pretende estender a CLI, você deve implementar o método Minitest.plugin_*_options
, que recebe dois parâmetros contendo a instância de OptionParser
e um um hash com as opções, incluindo a instância de IO para saída ($stdout
). Você poderia adicionar novas opçnoes, como por exemplo para definir se a saída dos testes deve ser colorida ou não.
module Minitest
def self.plugin_utils_options(opts, options)
opts.on('--color', 'Display colored output') do |color|
options[:color] = color
end
end
end
O reporter padrão do Minitest aceita múltiplos reporters: trata-se do da classe Minitest::CompositeReporter
. Para criar reporters personalizados ou outras extensões, você deve implementar o método Minitest.plugin_*_init
, que pode adicionar seu reporter à lista do composite.
module Minitest
def self.plugin_utils_init(options)
Minitest.reporter << GrowlReporter.new(options[:io], options)
end
end
Agora que você já sabe como o sistema de plugins funciona, vamos criar um reporter personalizado.
Criando um reporter personalizado
Veja como é a saída do reporter padrão do Minitest:
Como você pode ver, não é muito útil. Por isso, vamos adicionar um pouco de cores.
Para que isso funcione, precisamos limpar os reporters adicionados ao reporter padrão. Você pode fazer isso no método Minitest.plugin_utils_init
.
module Minitest
def self.plugin_colored_reporter_init(options)
Minitest.reporter.reporters.clear
Minitest.reporter << ColoredReporter.new(options[:io], options)
end
end
Perceba que estamos usando uma classe ColoredReporter
; vamos definí-la.
class ColoredReporter < Minitest::StatisticsReporter
end
Agora você precisa definir os metódos usados na geração do relatório. O primeiro método que vamos implementar é o ColoredReporter#record
, executado logo após cada execução de teste. Ele permitirá fazer o progresso realtime de nossos testes, indicado com pontos e outros símbolos.
class ColoredReporter < Minitest::StatisticsReporter
def record(result)
super
result_code = result.result_code
io.print color(result_code, RESULT_CODE_TO_COLOR[result_code])
end
end
Não se esqueça de chamar o super
; isso é necessário pois a classe Minitest::StatisticsReporter
computa o resultado dos testes. Vamos precisar implementar o método color
, que é bastante simples e utiliza escapes como \e[31mTexto\e[0m
.
class ColoredReporter < Minitest::StatisticsReporter
RESULT_CODE_TO_COLOR = {
'S' => :yellow,
'.' => :green,
'F' => :red,
'E' => :red
}
COLOR_CODE = {
red: 31,
green: 32,
yellow: 33,
blue: 34,
none: 0
}
def record(result)
super
result_code = result.result_code
io.print color(result_code, RESULT_CODE_TO_COLOR[result_code])
end
def color(text, color = :none)
code = COLOR_CODE[color]
"\e[#{code}m#{text}\e[0m"
end
end
À seguir, vamos implementar o método ColoredReporter#report
. Ele é executado após a suíte de testes ser executada, e deve exibir quais testes falharam, assim como estatísticas sobre a execução da suíte. Neste caso, vamos delegar cada trecho de saída para métodos específicos.
class ColoredReporter < Minitest::StatisticsReporter
# ...
def report
super
io.puts "\n\n"
io.puts statistics
io.puts aggregated_results
io.puts summary
end
end
O método ColoredReporter#statistics
calculará o tempo de execução, assim como outras informações.
class ColoredReporter < Minitest::StatisticsReporter
# ...
def statistics
"Finished in %.6fs, %.4f runs/s, %.4f assertions/s." %
[total_time, count / total_time, assertions / total_time]
end
end
Já o método ColoredReporter#aggregated_results
irá exibir os testes que falharam ou foram pulados.
class ColoredReporter < Minitest::StatisticsReporter
# ...
def aggregated_results
filtered_results = results.sort_by {|result| result.skipped? ? 1 : 0 }
filtered_results.each_with_index.map { |result, i|
color("\n%3d) %s" % [i+1, result], result.skipped? ? :yellow : :red)
}.join + "\n"
end
end
Finalmente, vamos exibir um resumo da execução, contendo quantidade de asserções, testes, falhas e mais.
class ColoredReporter < Minitest::StatisticsReporter
# ...
def summary
summary = "%d runs, %d assertions, %d failures, %d errors, %d skips" %
[count, assertions, failures, errors, skips]
color = :green
color = :yellow if skips > 0
color = :red if errors > 0 || failures > 0
color(summary, color)
end
end
Pronto! Agora podemos visualizar o resultado da execução dos testes com muito mais facilidade.
Mais um exemplo
E se a gente quiser exibir notificações Growl (ou outro sistema de notificação qualquer)? Com a gem test_notifier, esta é uma tarefa bem simples.
class TestNotifierReporter < Minitest::StatisticsReporter
def report
super
stats = TestNotifier::Stats.new(:minitest, {
count: count,
assertions: assertions,
failures: failures,
errors: errors
})
TestNotifier.notify(status: stats.status, message: stats.message)
end
end
Adicione esse reporter assim como você fez com o ColoredReporter
.
module Minitest
def self.plugin_colored_reporter_init(options)
Minitest.reporter.reporters.clear
Minitest.reporter << ColoredReporter.new(options[:io], options)
Minitest.reporter << TestNotifierReporter.new
end
end
Ao executar os testes, você receberá uma notificação como esta se estiver usando Mac e Growl:
Finalizando
O Minitest é realmente muito leve e extensível. Eu nunca me importei com a sintaxe de asserção (expect
vs should
vs assert
vs must
/wont
), mas o feedback da CLI do RSpec era quem fazia toda a diferença, com comandos para executar individualmente os testes que falharam, assim como uma saída colorida que facilita bastante a visualização.
Agora que tenho o mesmo nível de feedback, eu provavelmente vou usar apenas o Minitest. O Rails possui muitas classes para testar seus componentes (mailers, jobs, generators, e outros), então em vez de entender como fazer isso com RSpec, posso simplesmente usá-las.