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.
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
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.