Sunday, August 16, 2015

What is Ruby power_assert gem & why you may need it

After upgrading from Ruby 2.1.3 to 2.2.2 I've noticed a new bundled gem called power_assert. It turned out that test-unit requires it for like a year now. It was a 2nd surprise, because I thought that everyone's moved to minitest many years ago & test-unit was left alone for the backward-compatibility sake.

A 'power assert' enabled test-unit has an enhanced version of assert() that can take a block & in a case of failure print values for an each object in a method chain. If no block is given to this new assert(), the old one version is invoked.

$ cat example-1.rb
require 'test/unit'

class Hello < Test::Unit::TestCase
  def test_smoke
    assert { 3.times.include? 10 }
  end
end

$ ruby example-1.rb | sed -n '/==/,/==/p'
===============================================================================
Failure:
      assert { 3.times.include? 10 }
                 |     |
                 |     false
                 #<Enumerator: 3:times>
test_smoke(Hello)
/home/alex/.rvm/gems/ruby-2.2.2@global/gems/power_assert-0.2.2/lib/power_assert.
rb:29:in `start'
example-1.rb:5:in `test_smoke'
     2:
     3: class Hello < Test::Unit::TestCase
     4:   def test_smoke
  => 5:     assert { 3.times.include? 10 }
     6:   end
     7: end
===============================================================================

As I understand, Kazuki Tsujimoto (the author of power_assert gem) got the idea for a pretty picture for a method chain from the Groovy language. Before power_assert gem we could only use Object.tap() for peeking into the chain:

> ('a'..'c').to_a.tap {|i| p i}.map {|i| i.upcase }
["a", "b", "c"]
[
  [0] "A",
  [1] "B",
  [2] "C"
]

Using power_assert we can write a enhanced version of Kernel.p(), where in the spirit of the new assert(), it prints a fancy picture if a user provides a block for it:

$ cat super_duper_p.rb
require 'power_assert'

def p *args
  if block_given?
    PowerAssert.start(Proc.new, assertion_method: __callee__) do |pa|
      val = pa.yield
      str = pa.message_proc.call
      if str == "" then Kernel.p(val) else puts str end
      val
    end
  else
    Kernel.p(*args)
  end
end

$ cat example-2.rb
require './super_duper_p'

p {3.times.to_a.map {|i| "i=#{i}" }.include? 3}
p [1,2,3], [4,5,6], "7"
p { [1,2,3] }

$ ruby example-2.rb
p {3.times.to_a.map {|i| "i=#{i}" }.include? 3}
     |     |    |                   |
     |     |    |                   false
     |     |    ["i=0", "i=1", "i=2"]
     |     [0, 1, 2]
     #<Enumerator: 3:times>
[1, 2, 3]
[4, 5, 6]
"7"
[1, 2, 3]

Unfortunately, it won't work in irb.

If you're like the rest of us who prefer minitest instead of test-unit, you'll need a separate gem for it.

No comments:

Post a Comment