class Minitest::Reporters::HtmlReporter
The default ERB template uses JQuery and Bootstrap, both of these are included by referencing the CDN sites
The report is generated using ERB. A custom ERB template can be provided but it is not required
On each test run all files in the reports directory are deleted, this prevents a build up of old reports
When using Minitest Specs, the number prefix is dropped from the name of the test so that it reads well
so that failing and skipped tests are at the top.
The reporter sorts the results alphabetically and then by results
a shared link
This is recommended to be used with a CI server, where the report is kept as an artifact and is accessible via
A reporter for generating HTML test reports
def compare_suites(suite_a, suite_b)
Test suites which have failing tests are given highest order
Test suites are first ordered by evaluating the results of the tests, then by test suite name
def compare_suites(suite_a, suite_b) return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures] return -1 if suite_a[:has_errors_or_failures] && !suite_b[:has_errors_or_failures] return 1 if !suite_a[:has_errors_or_failures] && suite_b[:has_errors_or_failures] return compare_suites_by_name(suite_a, suite_b) if suite_a[:has_skipps] && suite_b[:has_skipps] return -1 if suite_a[:has_skipps] && !suite_b[:has_skipps] return 1 if !suite_a[:has_skipps] && suite_b[:has_skipps] compare_suites_by_name(suite_a, suite_b) end
def compare_suites_by_name(suite_a, suite_b)
def compare_suites_by_name(suite_a, suite_b) suite_a[:name] <=> suite_b[:name] end
def compare_tests(test_a, test_b)
Tess which fail are given highest order
Tests are first ordered by evaluating the results of the tests, then by tests names
def compare_tests(test_a, test_b) return compare_tests_by_name(test_a, test_b) if test_fail_or_error?(test_a) && test_fail_or_error?(test_b) return -1 if test_fail_or_error?(test_a) && !test_fail_or_error?(test_b) return 1 if !test_fail_or_error?(test_a) && test_fail_or_error?(test_b) return compare_tests_by_name(test_a, test_b) if test_a.skipped? && test_b.skipped? return -1 if test_a.skipped? && !test_b.skipped? return 1 if !test_a.skipped? && test_b.skipped? compare_tests_by_name(test_a, test_b) end
def compare_tests_by_name(test_a, test_b)
def compare_tests_by_name(test_a, test_b) friendly_name(test_a) <=> friendly_name(test_b) end
def friendly_name(test)
def friendly_name(test) groups = test.name.scan(/(test_\d+_)(.*)/i) return test.name if groups.empty? "it #{groups[0][1]}" end
def html_file
def html_file "#{@reports_path}/#{@output_filename}" end
def initialize(args = {})
:mode - Useful for debugging, :terse suppresses errors and is the default, :verbose lets errors bubble up
:erb_template - the path to a custom ERB template, defaults to the supplied ERB template
:reports_dir - the directory the reports should be written to, defaults to 'test/html_reports'
:title - the title that will be used in the report, defaults to 'Test Results'
The constructor takes a hash, and uses the following keys:
def initialize(args = {}) super({}) defaults = { :title => 'Test Results', :erb_template => "#{File.dirname(__FILE__)}/../templates/index.html.erb", :reports_dir => ENV['MINITEST_HTML_REPORTS_DIR'] || 'test/html_reports', :mode => :safe, :output_filename => ENV['MINITEST_HTML_REPORTS_FILENAME'] || 'index.html', } settings = defaults.merge(args) @mode = settings[:mode] @title = settings[:title] @erb_template = settings[:erb_template] @output_filename = settings[:output_filename] reports_dir = settings[:reports_dir] @reports_path = File.absolute_path(reports_dir) end
def location(exception)
def location(exception) last_before_assertion = '' exception.backtrace.reverse_each do |s| break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/ last_before_assertion = s end last_before_assertion.sub(/:in .*$/, '') end
def message_for(test)
def message_for(test) suite = test.class name = test.name e = test.failure if test.passed? nil elsif test.skipped? "Skipped:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n" elsif test.failure "Failure:\n#{name}(#{suite}) [#{location(e)}]:\n#{e.message}\n" elsif test.error? "Error:\n#{name}(#{suite}):\n#{e.message}" end end
def passes
def passes count - failures - errors - skips end
def percent_errors_failures
def percent_errors_failures ((errors + failures) / count.to_f * 100).to_i end
def percent_passes
def percent_passes 100 - percent_skipps - percent_errors_failures end
def percent_skipps
def percent_skipps (skips / count.to_f * 100).to_i end
def report
def report super begin puts "Writing HTML reports to #{@reports_path}" erb_str = File.read(@erb_template) renderer = ERB.new(erb_str) tests_by_suites = tests.group_by { |test| test_class(test) } # taken from the JUnit reporter suites = tests_by_suites.map do |suite, tests| suite_summary = summarize_suite(suite, tests) suite_summary[:tests] = tests.sort { |a, b| compare_tests(a, b) } suite_summary end suites.sort! { |a, b| compare_suites(a, b) } result = renderer.result(binding) File.open(html_file, 'w') do |f| f.write(result) end # rubocop:disable Lint/RescueException rescue Exception => e puts 'There was an error writing the HTML report' puts 'This may have been caused by cancelling the test run' puts 'Use mode => :verbose in the HTML reporters constructor to see more detail' if @mode == :terse puts 'Use mode => :terse in the HTML reporters constructor to see less detail' if @mode != :terse raise e if @mode != :terse end # rubocop:enable Lint/RescueException end
def start
def start super puts "Emptying #{@reports_path}" FileUtils.mkdir_p(@reports_path) File.delete(html_file) if File.exist?(html_file) end
def summarize_suite(suite, tests)
def summarize_suite(suite, tests) summary = Hash.new(0) summary[:name] = suite.to_s tests.each do |test| summary[:"#{result(test)}_count"] += 1 summary[:assertion_count] += test.assertions summary[:test_count] += 1 summary[:time] += test.time end summary[:has_errors_or_failures] = (summary[:fail_count] + summary[:error_count]) > 0 summary[:has_skipps] = summary[:skip_count] > 0 summary end
def test_fail_or_error?(test)
def test_fail_or_error?(test) test.error? || test.failure end
def total_time_to_hms
def total_time_to_hms return ('%.2fs' % total_time) if total_time < 1 hours = (total_time / (60 * 60)).round minutes = ((total_time / 60) % 60).round.to_s.rjust(2, '0') seconds = (total_time % 60).round.to_s.rjust(2, '0') "#{hours}h#{minutes}m#{seconds}s" end