Skip to content

Commit e3ea4c7

Browse files
authored
Merge pull request #52036 from richardboehme/assert-difference-output
Improve error message when passing a proc to `assert_difference` or `assert_changes`
2 parents fdbe363 + 38e9695 commit e3ea4c7

File tree

3 files changed

+153
-11
lines changed

3 files changed

+153
-11
lines changed

activesupport/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
* Improve error message when using `assert_difference` or `assert_changes` with a
2+
proc by printing the proc's source code (MRI only).
13

4+
*Richard Böhme*, *Jean Boussier*
25

36
Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/activesupport/CHANGELOG.md) for previous changes.

activesupport/lib/active_support/testing/assertions.rb

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ def assert_difference(expression, *args, &block)
118118

119119
expressions.zip(exps, before) do |(code, diff), exp, before_value|
120120
actual = exp.call
121-
error = "#{code.inspect} didn't change by #{diff}, but by #{actual - before_value}"
121+
code_string = code.respond_to?(:call) ? _callable_to_source_string(code) : code
122+
error = "`#{code_string}` didn't change by #{diff}, but by #{actual - before_value}"
122123
error = "#{message}.\n#{error}" if message
123124
assert_equal(before_value + diff, actual, error)
124125
end
@@ -202,7 +203,8 @@ def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &b
202203

203204
after = exp.call
204205

205-
error = "#{expression.inspect} didn't change"
206+
code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
207+
error = "`#{code_string}` didn't change"
206208
error = "#{error}. It was already #{to.inspect}" if before == to
207209
error = "#{message}.\n#{error}" if message
208210
refute_equal before, after, error
@@ -249,7 +251,8 @@ def assert_no_changes(expression, message = nil, from: UNTRACKED, &block)
249251

250252
after = exp.call
251253

252-
error = "#{expression.inspect} changed"
254+
code_string = expression.respond_to?(:call) ? _callable_to_source_string(expression) : expression
255+
error = "`#{code_string}` changed"
253256
error = "#{message}.\n#{error}" if message
254257

255258
if before.nil?
@@ -276,6 +279,36 @@ def _assert_nothing_raised_or_warn(assertion, &block)
276279

277280
raise
278281
end
282+
283+
def _callable_to_source_string(callable)
284+
if defined?(RubyVM::AbstractSyntaxTree) && callable.is_a?(Proc)
285+
ast = begin
286+
RubyVM::AbstractSyntaxTree.of(callable, keep_script_lines: true)
287+
rescue SystemCallError
288+
# Failed to get the source somehow
289+
return callable
290+
end
291+
return callable unless ast
292+
293+
source = ast.source
294+
source.strip!
295+
296+
# We ignore procs defined with do/end as they are likely multi-line anyway.
297+
if source.start_with?("{")
298+
source.delete_suffix!("}")
299+
source.delete_prefix!("{")
300+
source.strip!
301+
# It won't read nice if the callable contains multiple
302+
# lines, and it should be a rare occurence anyway.
303+
# Same if it takes arguments.
304+
if !source.include?("\n") && !source.start_with?("|")
305+
return source
306+
end
307+
end
308+
end
309+
310+
callable
311+
end
279312
end
280313
end
281314
end

activesupport/test/test_case_test.rb

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_assert_no_difference_fail
5454
@object.increment
5555
end
5656
end
57-
assert_equal "\"@object.num\" didn't change by 0, but by 1.\nExpected: 0\n Actual: 1", error.message
57+
assert_equal "`@object.num` didn't change by 0, but by 1.\nExpected: 0\n Actual: 1", error.message
5858
end
5959

6060
def test_assert_no_difference_with_message_fail
@@ -63,7 +63,7 @@ def test_assert_no_difference_with_message_fail
6363
@object.increment
6464
end
6565
end
66-
assert_equal "Object Changed.\n\"@object.num\" didn't change by 0, but by 1.\nExpected: 0\n Actual: 1", error.message
66+
assert_equal "Object Changed.\n`@object.num` didn't change by 0, but by 1.\nExpected: 0\n Actual: 1", error.message
6767
end
6868

6969
def test_assert_no_difference_with_multiple_expressions_pass
@@ -157,7 +157,7 @@ def test_hash_of_expressions_with_message
157157
@object.increment
158158
end
159159
end
160-
assert_equal "Object Changed.\n\"@object.num\" didn't change by 0, but by 1.\nExpected: 0\n Actual: 1", error.message
160+
assert_equal "Object Changed.\n`@object.num` didn't change by 0, but by 1.\nExpected: 0\n Actual: 1", error.message
161161
end
162162

163163
def test_assert_difference_message_includes_change
@@ -167,7 +167,17 @@ def test_assert_difference_message_includes_change
167167
@object.increment
168168
end
169169
end
170-
assert_equal "\"@object.num\" didn't change by 5, but by 2.\nExpected: 5\n Actual: 2", error.message
170+
assert_equal "`@object.num` didn't change by 5, but by 2.\nExpected: 5\n Actual: 2", error.message
171+
end
172+
173+
def test_assert_difference_message_with_lambda
174+
skip if !defined?(RubyVM::AbstractSyntaxTree)
175+
176+
error = assert_raises Minitest::Assertion do
177+
assert_difference(-> { @object.num }, 1, "Object Changed") do
178+
end
179+
end
180+
assert_equal "Object Changed.\n`@object.num` didn't change by 1, but by 0.\nExpected: 1\n Actual: 0", error.message
171181
end
172182

173183
def test_hash_of_lambda_expressions
@@ -233,7 +243,19 @@ def test_assert_changes_with_to_option_but_no_change_has_special_message
233243
end
234244
end
235245

236-
assert_equal "\"@object.num\" didn't change. It was already 0.\nExpected 0 to not be equal to 0.", error.message
246+
assert_equal "`@object.num` didn't change. It was already 0.\nExpected 0 to not be equal to 0.", error.message
247+
end
248+
249+
def test_assert_changes_message_with_lambda
250+
skip if !defined?(RubyVM::AbstractSyntaxTree)
251+
252+
error = assert_raises Minitest::Assertion do
253+
assert_changes -> { @object.num }, to: 0 do
254+
# no changes
255+
end
256+
end
257+
258+
assert_equal "`@object.num` didn't change. It was already 0.\nExpected 0 to not be equal to 0.", error.message
237259
end
238260

239261
def test_assert_changes_with_wrong_to_option
@@ -349,7 +371,91 @@ def test_assert_no_changes_with_message
349371
end
350372
end
351373

352-
assert_equal "@object.num should not change.\n\"@object.num\" changed.\nExpected: 0\n Actual: 1", error.message
374+
assert_equal "@object.num should not change.\n`@object.num` changed.\nExpected: 0\n Actual: 1", error.message
375+
end
376+
377+
def test_assert_no_changes_message_with_lambda
378+
skip if !defined?(RubyVM::AbstractSyntaxTree)
379+
380+
error = assert_raises Minitest::Assertion do
381+
assert_no_changes -> { @object.num } do
382+
@object.increment
383+
end
384+
end
385+
assert_equal "`@object.num` changed.\nExpected: 0\n Actual: 1", error.message
386+
387+
check = Proc.new {
388+
@object.num
389+
}
390+
error = assert_raises Minitest::Assertion do
391+
assert_no_changes check do
392+
@object.increment
393+
end
394+
end
395+
assert_equal "`@object.num` changed.\nExpected: 1\n Actual: 2", error.message
396+
397+
check = lambda {
398+
@object.num
399+
}
400+
error = assert_raises Minitest::Assertion do
401+
assert_no_changes check do
402+
@object.increment
403+
end
404+
end
405+
assert_equal "`@object.num` changed.\nExpected: 2\n Actual: 3", error.message
406+
407+
error = assert_raises Minitest::Assertion do
408+
assert_no_changes -> { @object.num } do
409+
@object.increment
410+
end
411+
end
412+
assert_equal "`@object.num` changed.\nExpected: 3\n Actual: 4", error.message
413+
414+
error = assert_raises Minitest::Assertion do
415+
assert_no_changes ->(a = nil) { @object.num } do
416+
@object.increment
417+
end
418+
end
419+
assert_match(/#<Proc:0x.*changed/, error.message)
420+
end
421+
422+
def test_assert_no_changes_message_with_multi_line_lambda
423+
check = lambda {
424+
"title".upcase
425+
@object.num
426+
}
427+
error = assert_raises Minitest::Assertion do
428+
assert_no_changes check do
429+
@object.increment
430+
end
431+
end
432+
assert_match(/#<Proc:0x.*changed/, error.message)
433+
434+
check = lambda {
435+
"title".upcase
436+
@object.num
437+
}
438+
error = assert_raises Minitest::Assertion do
439+
assert_no_changes check do
440+
@object.increment
441+
end
442+
end
443+
assert_match(/#<Proc:0x.*changed/, error.message)
444+
end
445+
446+
def test_assert_no_changes_message_with_not_real_callable
447+
check = Object.new
448+
def check.call
449+
@object.num
450+
end
451+
check.instance_variable_set(:@object, @object)
452+
453+
error = assert_raises Minitest::Assertion do
454+
assert_no_changes check do
455+
@object.increment
456+
end
457+
end
458+
assert_match(/#<Object:0x.*changed/, error.message)
353459
end
354460

355461
def test_assert_no_changes_with_long_string_wont_output_everything
@@ -362,7 +468,7 @@ def test_assert_no_changes_with_long_string_wont_output_everything
362468
end
363469

364470
assert_match <<~output, error.message
365-
"lines" changed.
471+
`lines` changed.
366472
--- expected
367473
+++ actual
368474
@@ -10,4 +10,5 @@
@@ -403,7 +509,7 @@ def test_warning_is_not_logged_if_assertions_are_nested_correctly
403509
run_test_that_should_fail_but_not_log_a_warning
404510
end
405511
assert_not @out.string.include?("assert_nothing_raised")
406-
assert error.message.include?("(lambda)> changed")
512+
assert error.message.include?("`rand` changed")
407513
end
408514

409515
def test_fails_and_warning_is_logged_if_wrong_error_caught

0 commit comments

Comments
 (0)