Advantages of mocking frameworks for testing Ruby on Rails

Published 01 October 2010

When I started out writing tests for my Rails projects I never really saw the point of a mocking framework.\r\n\r\nAfter all Factory Girl made it so easy to just populate the database with the all the test data I needed, so why fiddle around with setting up expectations?\r\n\r\nBut as our \Matchbook\ product got bigger, and we added more and more tests, it took longer and longer to run the test suite.\r\n\r\nThis got to a point where we were losing the advantages of continuous testing, because every code change kicked off several minutes of tests.\r\n\r\nGiven that the biggest overhead seemed to be putting stuff in the database, then reading it straight back out, it seemed logical to try mocking some of this out.\r\n\r\nSo I decided to introduce \Ruby Double\ on a few tests which, once you overcome the frustratingly terse documentation, is exceedingly useful at speeding up tests.\r\n\r\nBut the unintended consequence which I had initially overlooked is that these mocks actually set expectations, and the tests fail when the expectations are not met.\r\n\r\nSo not only do you isolate what you’re testing (e.g. mock out the models when testing the controller), you also ensure that exactly the right path is followed through your code.\r\nAnd furthermore you know exactly the right methods are being called with exactly the right parameters.\r\n\r\nNow all our tests run much faster and I can sleep easy at night knowing my test coverage is more complete and more accurate.\r\nAlso I can run the same test with all sorts of different data sets without having to stick it all in the database first, making my test code shorter and more readable.\r\n\r\nHere’s an extract from a controller test which uses RSpec and Ruby Double to mock and stub out some method calls:\r\n\r\n@@@ ruby\r\n def prepare_user\r\n activate_authlogic\r\n user = Factory.build(:user, :user_type => type, :account => Factory.build(:account))\r\n stub(user).id { 2 }\r\n stub(user).account_id { 1 }\r\n stub(controller).current_user { user }\r\n end\r\n\r\n describe \"when logged in\" do\r\n before { prepare_user }\r\n describe \"and I hit the\" do\r\n\r\n describe \"edit action\" do\r\n before do\r\n mock(CallList).find_for_user(\"1234\", @user) { CallList.new }\r\n get :edit, :id => 1234\r\n end\r\n specify { response.should render_template(:edit) }\r\n end\r\n\r\n describe \"update action\" do\r\n before do\r\n @call_list = CallList.new\r\n mock(CallList).find_for_user(\"1234\", @user) { @call_list }\r\n end\r\n\r\n describe \"successfully\" do\r\n before do\r\n mock(call_list).valid? { true }\r\n post \“update\”, {:id => 1234}\r\n end\r\n specify { response.should redirect_to(call_lists_path) }\r\n specify { flash[:success].should_not be_nil }\r\n end\r\n\r\n describe \“and get an error\” do\r\n before { post \“update\”, {:id => 1234} }\r\n subject { response }\r\n it { should render_template(:edit) }\r\n end\r\n end\r\n end\r\n end\r\n@@@