class Minitest::Reporters::HtmlReporter
A reporter for generating HTML test reports This is recommended to be used with a CI server, where the report is kept as an artifact and is accessible via a shared link
The reporter sorts the results alphabetically and then by results so that failing and skipped tests are at the top.
When using Minitest Specs, the number prefix is dropped from the name of the test so that it reads well
On each test run all files in the reports directory are deleted, this prevents a build up of old reports
The report is generated using ERB. A custom ERB template can be provided but it is not required The default ERB template uses JQuery and Bootstrap, both of these are included by referencing the CDN sites
Attributes
The title of the report
Public Class Methods
The constructor takes a hash, and uses the following keys: :title - the title that will be used in the report, defaults to 'Test Results' :reports_dir - the directory the reports should be written to, defaults to 'test/html_reports' :erb_template - the path to a custom ERB template, defaults to the supplied ERB template :mode - Useful for debugging, :terse suppresses errors and is the default, :verbose lets errors bubble up :output_filename - the report's filename, defaults to 'index.html'
# File lib/minitest/reporters/html_reporter.rb, line 57 def initialize(args = {}) super({}) defaults = { :title => 'Test Results', :erb_template => "#{File.dirname(__FILE__)}/../templates/index.html.erb", :reports_dir => 'test/html_reports', :mode => :safe, :output_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
Public Instance Methods
Trims off the number prefix on test names when using Minitest Specs
# File lib/minitest/reporters/html_reporter.rb, line 45 def friendly_name(test) groups = test.name.scan(/(test_\d+_)(.*)/i) return test.name if groups.empty? "it #{groups[0][1]}" end
The number of tests that passed
# File lib/minitest/reporters/html_reporter.rb, line 25 def passes count - failures - errors - skips end
The percentage of tests that failed
# File lib/minitest/reporters/html_reporter.rb, line 40 def percent_errors_failures ((errors + failures) / count.to_f * 100).to_i end
The percentage of tests that passed, calculated in a way that avoids rounding errors
# File lib/minitest/reporters/html_reporter.rb, line 30 def percent_passes 100 - percent_skipps - percent_errors_failures end
The percentage of tests that were skipped
# File lib/minitest/reporters/html_reporter.rb, line 35 def percent_skipps (skips / count.to_f * 100).to_i end
Called by the framework to generate the report
# File lib/minitest/reporters/html_reporter.rb, line 88 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
# File lib/minitest/reporters/html_reporter.rb, line 79 def start super puts "Emptying #{@reports_path}" FileUtils.mkdir_p(@reports_path) File.delete(html_file) if File.exist?(html_file) end
Private Instance Methods
Test suites are first ordered by evaluating the results of the tests, then by test suite name Test suites which have failing tests are given highest order Tests suites which have skipped tests are given second highest priority
# File lib/minitest/reporters/html_reporter.rb, line 139 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
# File lib/minitest/reporters/html_reporter.rb, line 128 def compare_suites_by_name(suite_a, suite_b) suite_a[:name] <=> suite_b[:name] end
Tests are first ordered by evaluating the results of the tests, then by tests names Tess which fail are given highest order Tests which are skipped are given second highest priority
# File lib/minitest/reporters/html_reporter.rb, line 154 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
# File lib/minitest/reporters/html_reporter.rb, line 132 def compare_tests_by_name(test_a, test_b) friendly_name(test_a) <=> friendly_name(test_b) end
# File lib/minitest/reporters/html_reporter.rb, line 124 def html_file "#{@reports_path}/#{@output_filename}" end
taken from the JUnit reporter
# File lib/minitest/reporters/html_reporter.rb, line 204 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
based on #message_for(test) from the JUnit reporter
# File lib/minitest/reporters/html_reporter.rb, line 187 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
based on analyze_suite from the JUnit reporter
# File lib/minitest/reporters/html_reporter.rb, line 172 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
# File lib/minitest/reporters/html_reporter.rb, line 167 def test_fail_or_error?(test) test.error? || test.failure end
# File lib/minitest/reporters/html_reporter.rb, line 213 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