[Unit Testing for Zombie] 04. Mock and Stub
STUBBING
Notice how the show_author_summary method in tweet is calling the zombie_summarymethod inside zombie. This isn't good! Stub out the zombie_summary method in the test below.
tweet.rb
class Tweet < ActiveRecord::Base belongs_to :zombie validates :status, presence: true, length: {within: 3..140, allow_blank: true} validates :zombie, presence: true def brains? status =~ /(brains|breins)/i end def show_author_summary self.status = self.zombie.zombie_summary end def status_image image = ZwitPic.get_status_image(self.id) # This is returning an array {image_name: image[0], image_url: image[1]} end end
zombie.rb
class Zombie < ActiveRecord::Base has_many :tweets validates :name, presence: true, length: {maximum: 15}, uniqueness: {case_sensitive: false} validates :graveyard, presence: true def to_param name end def zombie_summary "#{name} lives in #{graveyard} and has #{self.tweets.size} tweets" end def geolocate loc = Zoogle.graveyard_locator(self.graveyard) "#{loc[:latitude]}, #{loc[:longitude]}" end def geolocate_with_object loc = Zoogle.graveyard_locator(self.graveyard) "#{loc.latitude}, #{loc.longitude}" end def avatar_url "http://zombitar.com/#{id}.jpg" end end
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "show_author_summary should set status to zombie summary" do @tweet.show_author_summary assert_equal @tweet.zombie.zombie_summary, @tweet.status, 'tweet status does not contain zombie summary' end end
Answer:
First use stubs to call zombie_summary method, it is a fake calling, but set the value of status.
It should before the @tweet.show_author_summary line.
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "show_author_summary should set status to zombie summary" do @tweet.zombie.stubs(:zombie_summary) @tweet.show_author_summary assert_equal @tweet.zombie.zombie_summary , @tweet.status, 'tweet status does not contain zombie summary' end end
MOCKING
Create another test to make sure zombie_summary is actually called in theshow_author_summary method using a mock.
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "show_author_summary should call zombie_summary" do @tweet.zombie.expects(:zombie_summary) @tweet.show_author_summary end end
STUB + ASSERT
Notice how the status_image method in Tweet calls ZwitPic.get_status_image, potentially a remote server call. Create a test which mocks this method and ensures the correct params get sent.
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "status_image calls the ZwitPic get_status_image api" do ZwitPic.expects(:get_status_image).with(@tweet.id) @tweet.status_image end end
Dont forget to call status_image mthod after stubs the get_status_image, because get_status_image is stubbed, but it is inisde status_images method.
STUBS & MOCKING
Notice how the status_image method now has a return value which is expecting image to be an Array. Modify the mock to return an array with two string values.
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "status_image calls the ZwitPic get_status_image api" do ZwitPic.expects(:get_status_image).with(@tweet.id).returns(['Scary Zombie', 'http://zwitpic.com/scary_zombie.png']) @tweet.status_image end end
RETURNING PROPER RESULTS
Notice that the status_image method on Tweet now returns an HTML image tag with an alt attribute set to the image name, and the ZwitPic.get_status_image method is now returning a hash. Write a test which stubs, instead of mocks, get_status_image and tests that @tweet.status_image returns the proper result (which should also be an image tag).
def status_image status_image = ZwitPic.get_status_image(self.id) "<img src='#{status_image[:url]}' alt='#{status_image[:name]}' />".html_safe end
Answer:
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "status_image returns a properly formated HTML image element with alt and src" do #using stubs to test get_status_image is called and set the value #with(): send the param #returns(): mock a return value ZwitPic.stubs(:get_status_image).with(@tweet.id).returns({name: "Gegg", url: "http://monxp.com/monkey.png"}) assert_equal "<img src='http://monxp.com/monkey.png' alt='Gegg' />", @tweet.status_image end end
OBJECT STUB
Notice how the get_status_image is now returning an object. Using an object stub, refactor the previously created test.
def status_image image = ZwitPic.get_status_image(self.id) "<img src='#{image.url}' alt='#{image.name}' />".html_safe end
Answer:
class TweetTest < ActiveSupport::TestCase def setup @tweet = tweets(:hello_world) end test "status_image returns a properly formated HTML image element with alt and src" do image = stub(name: "Yummy brain I ate last night", url: "http://zwitpic.com/2.jpg") ZwitPic.stubs(:get_status_image).with(@tweet.id).returns(image) assert_equal "<img src='http://zwitpic.com/2.jpg' alt='Yummy brain I ate last night' />", @tweet.status_image end end