Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Minitest reports the wrong line number when an assertion fails inside a block

Tags:

ruby

minitest

I have written an assertion that collects new records created while it yields to a block. Here's an example, with a failing assertion inside that block:

  product =
    assert_latest_record Product do  #  line 337
      post :create, 
           :product => { ... }
      assert false  #  line 340
    end

The source of my assertion is below, but I don't think it's relevant. It does not intercept Minitest exceptions, or even call rescue or ensure.

The problem is when an assertion inside that block fails. The fault diagnostic message reports the line number as 337 the line of the outer assertion, not 340, the line of the inner assertion that failed. This is important if, for example, my colleagues have written a run-on test with way too many lines in it; isolating a failing line becomes more difficult.

Why doesn't Minitest report the correct line number?


The source:

##
# When a test case calls methods that write new ActiveModel records to a database,
# sometimes the test needs to assert those records were created, by fetching them back
# for inspection. +assert_latest_record+ collects every record in the given model or
# models that appear while its block runs, and returns either a single record or a ragged
# array.
#
# ==== Parameters
#
# * +models+ - At least 1 ActiveRecord model or association.
# * +message+ - Optional string or ->{block} to provide more diagnostics at failure time.
# * <code>&block</code> - Required block to call and monitor for new records.
#
# ==== Example
#
#   user, email_addresses =
#     assert_latest_record User, EmailAddress, ->{ 'Need moar records!' } do
#       post :create, ...
#     end
#   assert_equal 'franklyn', user.login  #  1 user, so not an array
#   assert_equal 2, email_addresses.size
#   assert_equal '[email protected]', email_addresses.first.mail
#   assert_equal '[email protected]', email_addresses.second.mail
#
# ==== Returns
#
# The returned value is a set of one or more created records. The set is normalized,
# so all arrays of one item are replaced with the item itself.
#
# ==== Operations
#
# The last argument to +assert_latest_record+ can be a string or a callable block.
# At failure time the assertion adds this string or this block's return value to
# the diagnostic message.
#
# You may call +assert_latest_record+ with anything that responds to <code>.pluck(:id)</code>
# and <code>.where()</code>, including ActiveRecord associations:
#
#   user = User.last
#   email_address =
#     assert_latest_record user.email_addresses do
#       post :add_email_address, user_id: user.id, ...
#     end
#   assert_equal '[email protected]', email_address.mail
#   assert_equal email_address.user_id, user.id, 'This assertion is redundant.'
#
def assert_latest_record(*models, &block)
  models, message = _get_latest_record_args(models, 'assert')
  latests = _get_latest_record(models, block)
  latests.include?(nil) and _flunk_latest_record(models, latests, message, true)
  pass  #  Increment the test runner's assertion count
  return latests.size > 1 ? latests : latests.first
end

##
# When a test case calls methods that might write new ActiveModel records to a
# database, sometimes the test must check that no records were written.
# +refute_latest_record+ watches for new records in the given class or classes
# that appear while its block runs, and fails if any appear.
#
# ==== Parameters
#
# See +assert_latest_record+.
#
# ==== Operations
#
#   refute_latest_record User, EmailAddress, ->{ 'GET should not create records' } do
#     get :index
#   end
#
# The last argument to +refute_latest_record+ can be a string or a callable block.
# At failure time the assertion adds this string or this block's return value to
# the diagnostic message.
#
# Like +assert_latest_record+, you may call +refute_latest_record+ with anything
# that responds to <code>pluck(:id)</code> and <code>where()</code>, including
# ActiveRecord associations.
#
def refute_latest_record(*models, &block)
  models, message = _get_latest_record_args(models, 'refute')
  latests = _get_latest_record(models, block)
  latests.all?(&:nil?) or _flunk_latest_record(models, latests, message, false)
  pass
  return
end

##
# Sometimes a test must detect new records without using an assertion that passes
# judgment on whether they should have been written. Call +get_latest_record+ to
# return a ragged array of records created during its block, or +nil+:
#
#   user, email_addresses, posts =
#     get_latest_record User, EmailAddress, Post do
#       post :create, ...
#     end
#
#   assert_nil posts, "Don't create Post records while creating a User"
#
# Unlike +assert_latest_record+, +get_latest_record+ does not take a +message+ string
# or block, because it has no diagnostic message.
#
# Like +assert_latest_record+, you may call +get_latest_record+ with anything
# that responds to <code>.pluck(:id)</code> and <code>.where()</code>, including
# ActiveRecord associations.
#
def get_latest_record(*models, &block)
  assert models.any?, 'Call get_latest_record with one or more ActiveRecord models or associations.'
  refute_nil block, 'Call get_latest_record with a block.'
  records = _get_latest_record(models, block)
  return records.size > 1 ? records : records.first
end  #  Methods should be easy to use correctly and hard to use incorrectly...

def _get_latest_record_args(models, what) #:nodoc:
  message = nil
  message = models.pop unless models.last.respond_to?(:pluck)
  valid_message = message.nil? || message.kind_of?(String) || message.respond_to?(:call)
  models.length > 0 && valid_message and return models, message

  raise "call #{what}_latest_record(models..., message) with any number\n" +
        'of Model classes or associations, followed by an optional diagnostic message'
end
private :_get_latest_record_args

def _get_latest_record(models, block) #:nodoc:
  id_sets = models.map{ |model| model.pluck(*model.primary_key) }  #  Sorry about your memory!
  block.call
  record_sets = []

  models.each_with_index do |model, index|
    pk = model.primary_key
    set = id_sets[index]

    records =
      if set.length == 0
        model
      elsif pk.is_a?(Array)
        pks = pk.map{ |k| "`#{k}` = ?" }.join(' AND ')
        pks = [ "(#{pks})" ] * set.length
        pks = pks.join(' OR ')
        model.where.not(pks, *set.flatten)
      else
        model.where.not(pk => set)
      end

    records = records.order(pk).to_a
    record_sets.push records.size > 1 ? records : records.first
  end

  return record_sets
end
private :_get_latest_record

def _flunk_latest_record(models, latests, message, polarity) #:nodoc:
  itch_list = []

  models.each_with_index do |model, index|
    records_found = latests[index] != nil
    records_found == polarity or itch_list << model.name
  end

  itch_list = itch_list.join(', ')
  diagnostic = "should#{' not' unless polarity} create new #{itch_list} record(s) in block"
  message = nil if message == ''
  message = message.call.to_s if message.respond_to?(:call)
  message = [ message, diagnostic ].compact.join("\n")
  raise Minitest::Assertion, message
end
private :_flunk_latest_record
like image 812
Phlip Avatar asked Dec 09 '25 17:12

Phlip


1 Answers

You could try to configure it to log exceptions in test_helper.rb:

def MiniTest.filter_backtrace(backtrace)
  backtrace
end

I'm not sure if this is the default, but depending on your configuration, the backtrace might not be shown.

like image 128
mahemoff Avatar answered Dec 11 '25 15:12

mahemoff