Mocking is a beautiful thing. There are no words to describe the immense pleasure I get from writing a good mocked test. When I first started writing tests, I had been unexposed to the glory of mocking. The days of relying on fixtures and writing brittle tests are over.
Stub and Mock are brothers that live together. Stub knew some things about Mock, and he had a pretty good understanding of how Mock would act. It didn’t matter much to Stub what Mock did. Stub knew that Mock would sometimes want to cook breakfast for Stub, and Stub knew to give Mock some eggs. Mock sometimes didn’t have any time to make breakfast for Stub, but it didn’t bother Stub too much. Mock always expected Stub to make lunch. If Stub didn’t make lunch, Mock would complain until Stub made him lunch. For dinner, they would both usually get take out. One night Mock began to make dinner, and Stub was baffled, he complained to Mock because he was behaving in a way Stub wasn’t used to. Mock explained to Stub, that sometimes he will make dinner too, and he needs some eggs. Stub brought Mock some eggs, and Mock made dinner. The next night, they got take out as usual, but Stub didn’t mind too much.
def show @article = Article.find params[:id] end def stub_article stub('stub article', :id => 1, :title => 'title', :author => 'author', :post => stub_everything('post')) end def test_should_show_an_article_on_GET_to_show article = stub_article Article.expects(:find).with('1').returns article get :show, :id => '1' assert_equal article, assigns(:article) assert_response :success assert_template 'show' end
The Stub Article will understand and respond to #id, #title, #author, and #post.
When calling @article.anyothermethod in the view, it will complain. The #post
stub_everything, which is a lazy way to allow something like this:
@article.post.anyothermethod. An Article shouldn’t care about what it’s Post
can do, but just know that it can have and call its Post.
The functional text expects that Article will call find with “1”, and return the Stub Article. If any other calls are made on Article, the test complain. If the find call is made with something other than “1”, the test will complain.
A great deal of faith must be placed in the
find method on Article that it
does work as advertised. I can assume that there are tests written for that
method – therefore I don’t need to test it during the functional test. Instead,
I make sure that the instance variable I need is set to the Article Stub, and
that it isn’t calling methods that it can’t recognize.
class Article validates_presence_of :title, :author def before_create self.post = Post.create! :title => title, :user => User.find(:first, :conditions => 'admin = true') end end
We need to write a unit test for Article to make sure that a Post is created when an Article is made. We could use expectations – but when getting to the low level of testing a model, I prefer to use a more state-based approach. This could require the use of fixtures. We would need to set up a complex network of Users, Posts, and Articles. If you note an earlier post, I really don’t like fixtures much. Here is the approach I used:
def test_should_create_a_post_for_the_article_before_create article = Article.new(:title => 'title') User.stubs(:find).returns User.new assert ! article.post article.save(false) assert article.post assert_equal Post.find(:first), article.reload.post end
The test uses my newly found and loved method: #.save(false). My test is only
testing that a Post is created when creating an Article. If something changes
(a validated attribute is added or removed) on Post – my test will complain,
because that Post will not be created. I don’t care about the User, if
something changes on a User, my test will not break. I can make the assumption
that the User has its own tests. I don’t care that my Article is invalid (I
don’t set the
author when creating the article), because I know I have another
test for Article creation.
This is my first attempt at preventing broken test chain reactions. It annoys me when I make a small change to one Model in a complex application, because most of my time is spent fixing tests in related functional and unit tests, and updating fixtures. Even though the application works fine, I spend all my time baby sitting tests.