Have you ever tried testing a method that invokes a block? Recently, I was working on writing tests for a method that was being called in multiple workers to do slightly different things. This method accepted a unit of code or block that was customized for each worker. Luckily, RSpec provides very helpful ‘yield’ matchers to test this type of functionality.

Ruby blocks are simply anonymous chunks of code that can be injected someplace into a method with the yield keyword. This allows us to have one method that can work in various different ways without having to write multiple different methods.

RSpec provides four related matchers that allow you to test whether or not a method yields what you were expecting.

Rspec Yield Matchers

Let’s have some fun with a few examples of blocks and how to test them. These examples will print out lines from my favorite Norwegian song, The Fox (What Does the Fox Say?) by Ylvis. Do yourself a favor and watch this video if you haven’t already. Now on to the code:

Block Invoked Once

def ode_to_best_song_ever(&_block)
  animal, sound = "fish", "blub"
  puts " Ducks say quack"
  yield(“fish”, “blub)
  puts “And the seal goes ow ow ow”
  puts "But there's one sound. That no one knows. What does the fox say?"
end

ode_to_best_song_ever do |animal, sound|
  puts "#{animal.titleize} goes #{sound}"
end

# output
Ducks say quack
Fish goes blub
And the seal goes ow ow ow
But there's one sound. That no one knows. What does the fox say?

# tests
yield_control matcher:
it ‘yields the expected result’ do
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .to yield_control.at_most(1).times
end

yield_with_args matcher:
it 'yields the expected result’ do
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .to yield_with_args("fish","blub)
end

yield_with_no_args matcher:
it 'yields the expected result’ do
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .not_to yield_with_no_args
end

Block Invoked Multiple Times

Let’s change our method to yield multiple times so we can test again with the yield_control matcher and try out the yield_successive_args matcher.

def ode_to_best_song_ever(&_block)
  sound = "Wa-pa-pa-pa-pa-pa-pow!"
  3.times { yield(sound) }
  puts "What the fox say?"
end

ode_to_best_song_ever do |sound|
  puts "#{sound}"
end

# output
"Wa-pa-pa-pa-pa-pa-pow!"
"Wa-pa-pa-pa-pa-pa-pow!"
"Wa-pa-pa-pa-pa-pa-pow!"
"What the fox say?"

# tests
yield_control matcher:
it ‘yields the expected result’ do
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .to yield_control.exactly(3).times
end

yield_successive_args matcher:
it ‘yields the expected result’ do
  expected_args = ["Wa-pa-pa-pa-pa-pa-pow!"] * 3
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .to yield_successive_args(*expected_args)
end

Block Invoked Via Iterator

Now let’s change our method one more time to utilize an iterator to print out more animal sounds and test using the yield_successive_args matcher again.

def ode_to_best_song_ever(&_block)
  sounds = {dog: “woof”, cat: “meow, bird: “tweet”, cow: “moo”}
  sounds.each { |animal, sound| yield(animal, sound) }
  puts "But there's one sound. That no one knows. What does the fox say?"
end

ode_to_best_song_ever do |animal, sound|
  puts "#{animal.to_s.titleize} goes #{sound}"
end

# output
Dog goes woof
Cat goes meow
Bird goes tweet
Cow goes moo
But there's one sound. That no one knows. What does the fox say?

# tests
yield_control matcher:
it 'yields the expected result' do
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .to yield_control.exactly(4).times
end

yield_successive_args matcher:
it 'yields the expected result’ do
  expected_args = ["dog","woof"],["cat","meow"],["bird","tweet"],[“cow”,”moo”]
  expect {|block| described_class.ode_to_best_song_ever(&block)}
    .to yield_successive_args(expected_args)
end

What does the fox say?

Reading about testing blocks is likely not the most exciting task of your day so I hope these fun examples come in handy and put a smile on your face. Happy testing!

For more than 17 years, gap intelligence has served manufacturers and sellers by providing world-class services monitoring, reporting, and analyzing the 4Ps: prices, promotions, placements, and products. Email us at info@gapintelligence.com or call us at 619-574-1100 to learn more.