Thursday, June 14, 2007

Testing Rails controllers with mock objects

One of the steps in my 15 TDD steps to create a Rails application article was using the mocking technique.
Mock objects were used in a test for a controller method. From the feedback I received I noticed that this technique is not very popular and requires a more detailed explanation. This article describes why you might want to use mock objects for testing your controllers and how it simplifies the testing setup and code. The mocking library I use in this article is Mocha.

Rails controllers

Before I go into details about testing Rails controllers, let's clarify what is the responsibility of a Rails controller.

Ideally, a controller method should only call one method on a model class. Sometimes the method on the model that we call is some kind of finder like Person.find(:all). The result of the method should then be passed to the view.
A Rails controller should be a thin layer between your model classes and the view part.

A sample implementation looks like that:

def learn
@word = Word.random
end

As we see, we call the 'random method on the Word class which represents some model part of our application. Then we pass the result to the view via @word.

Testing Rails controllers

Being aware of that, we know what we want to test:
A learn method should:

  1. call the Word.random method

  2. pass the result to the view


Problems with traditional approach to testing Rails controllers
Now, what if we simply construct the test like this:

def test_learn_passes_a_random_word
get 'learn'
assert_kind_of Word, assigns('word')
end

The first problem here is that it calls the Word.random method, thus accesses the (test) database. It means we need to have some fixtures loaded so that the random method can retrieve some word from it.
The second problem is that we only test the type (kind_of) of the object assigned to @word. It means we don't fully test the method. We could imagine an implementation of the learn method that passes the tests but does the wrong thing (by assigning any other object to @word). Not ideal.

Testing with mock objects

Fortunately, we can use mock objects here and solve both problems.
We will replace the Word.random method with a method that always returns an object that we prepared before. Another advantage is that:

We don't connect to the database from our unit tests which is A Good Thing.


The object that we prepare before can then be used to assert whether the one passed to the view was the same as the result of calling Word.random method.

random_word = Word.new
Word.expects(:random).returns(random_word)

As we see, we prepare a random_word object, which is simply a new instance of a Word class. The next line does two things:

  • expects(:random) - sets our expectation that the random method will be called

  • returns(random_word) - replaces the real implementation of the 'random' method with an implementation that always returns the random_word object.


Btw, we didn't have to create a new instance of Word here. We could have used Object() there and that would work fine. The crucial part is that we compare the objects we return from Word.random with the one assigned to @word.

What is an expectation?

An expectation is almost like an assertion. The difference is that we set it before the actual call to the method that we test. The test will fail if the expectation isn't met.

We now extend the test with a call to the 'learn' method that we test:

random_word = Word.new
Word.expects(:random).returns(random_word)
get 'learn'

This test should now pass with the following implementation:

def learn
@word = Word.random
end


We also want to test that the object was assigned to @word, so we need an additional line with assertion:

def test_learn_passes_a_random_word
random_word = Word.new
Word.expects(:random).returns(random_word)
get 'learn'
assert_equal random_word, assigns('word')
end


The assertion tests the equality of the random_word object and the @word (we access it using assigns('word')). It's possible only thanks to the fact that we replaced the real Word.random method and have a reference to the result of this method.
Benefits

  • not connecting to a database
  • no need for preparing fixtures
  • faster tests
  • controller tests are less fragile for model changes

Drawbacks

  • some developers may find the tests less readable
  • if you rename the model method, the controller tests will still pass
  • increased need for some integration tests - Selenium or standard Rails integration tests


Conclusion

I hope that this article sheds more light on what are mock objects. I strongly recommend trying to add some tests like that to your test suite. If you want to read more about mocking you should first go to the classic Martin Fowler's article on Mock Objects. After that go to the Jay Field's blog. You can also find some interesting examples on the Mocha website.

7 comments:

Florian said...

Yeah, and what's that got to do with python (such that it apears on planet.python.org) ?

Fuzzyman said...

Really - you mean there was a blog entry on Planet Python that wasn't about Python? My goodness, that has never happened before...

Florian said...

I don't know what's difficult to grasp about the concept of tagging posts with python, and then feed that subset of your posts to planetpython.

Fuzzyman said...

I've just been to Planet Python. In the first few entries is a 'maths joke' a post about Web 2.0 stuff and another post about the opera browser.

I assume you object to those just as strongly - or is it because this is about our arch enemy Ruby?

Florian said...

I can't go around telling this everybody who posts offtopic to planet.python.org now can I?

Ya I believe Ruby is pretty vile a language, and that before long the hype will be followed by a big bang and a rude awakening for those who put some of their eggs into that particular basket.

So of course, posting something like an antitopic, you'll inevitably attact comments to the effect that you're offtopic.

Btw, are you going to be on the europython this year? We could meet up and discuss this more in real time/life ;)

Miles said...

Despite the other comments on this particular post, I thought I'd let you know that it is appreciated (from a Ruby and Rails perspective, anyway). I'm not sure why the Python community hates Ruby. However, I am enjoying the testing posts, and I'd love it if you'd keep them up. Thanks.

Anonymous said...

情趣用品,情趣用品,情趣用品,情趣用品,情趣,情趣,情趣,情趣,按摩棒,震動按摩棒,微調按摩棒,情趣按摩棒,逼真按摩棒,G點,跳蛋,跳蛋,跳蛋,性感內衣,飛機杯,充氣娃娃,情趣娃娃,角色扮演,性感睡衣,SM,潤滑液,威而柔,香水,精油,芳香精油,自慰套,自慰,性感吊帶襪,吊帶襪,情趣用品加盟AIO交友愛情館,情人歡愉用品,美女視訊,情色交友,視訊交友,辣妹視訊,美女交友,嘟嘟成人網,成人網站,A片,A片下載,免費A片,免費A片下載愛情公寓,情色,舊情人,情色貼圖,情色文學,情色交友,色情聊天室,色情小說,一葉情貼圖片區,情色小說,色情,色情遊戲,情色視訊,情色電影,aio交友愛情館,色情a片,一夜情,辣妹視訊,視訊聊天室,免費視訊聊天,免費視訊,視訊,視訊美女,美女視訊,視訊交友,視訊聊天,免費視訊聊天室,情人視訊網,影音視訊聊天室,視訊交友90739,成人影片,成人交友,美女交友,微風成人,嘟嘟成人網,成人貼圖,成人電影,A片,豆豆聊天室,聊天室,UT聊天室,尋夢園聊天室,男同志聊天室,UT男同志聊天室,聊天室尋夢園,080聊天室,080苗栗人聊天室,6K聊天室,女同志聊天室,小高聊天室,上班族聊天室,080中部人聊天室,同志聊天室,聊天室交友,中部人聊天室,成人聊天室,一夜情聊天室,情色聊天室,寄情築園小遊戲情境坊歡愉用品,情趣用品,成人網站,情人節禮物,情人節,AIO交友愛情館,情色,情色貼圖,情色文學,情色交友,色情聊天室,色情小說,七夕情人節,色情,情色電影,色情網站,辣妹視訊,視訊聊天室,情色視訊,免費視訊聊天,美女視訊,視訊美女,美女交友,美女,情色交友,成人交友,自拍,本土自拍,情人視訊網,視訊交友90739,生日禮物,情色論壇,正妹牆,免費A片下載,AV女優,成人影片,色情A片,成人論壇,情趣,免費成人影片,成人電影,成人影城,愛情公寓,成人影片,保險套,舊情人,微風成人,成人,成人遊戲,成人光碟,色情遊戲,跳蛋,按摩棒,一夜情,男同志聊天室,肛交,口交,性交,援交,免費視訊交友,視訊交友,一葉情貼圖片區,性愛,視訊,視訊聊天,A片,A片下載,免費A片,嘟嘟成人網,寄情築園小遊戲,女同志聊天室,免費視訊聊天室,一夜情聊天室,聊天室