jueves, 10 de julio de 2008

RSpec: fixtures vs mocks

Hi there!,

It's been a while since the last time I wrote but better late than ever ;)
This time I'm going to dissert a little about the way we write our specs. In particular, as I am a rookie with rspec, I would like to know which is the right way to load data into our tests.

For that matter, I've found that there are mainly two approaches for that: 1) use fixtures or 2) use mocks/stubs instead.

Using Fixtures

Fixtures enable us to define data for our models in a human readable way using YAML syntax. A simple example, as you may have seen for Test:Unit, is shown below.

#spec/fixtures/user.yml
david:
id: 1
name: David Hasselhof

birthday: 1500-01-01

profession: Be-a-god
hashed_password: <%= EncryptionLibrary.encrypt("old_password") %>


Then, in your spec you only have to make reference to the fixture you want to load to make that happened. This approach is very handly for two reasons:
  1. This decouples the data to be tested with the specs (tests) itself.
  2. Is very simple to understand for small bunch of data.
But, It has several disadvantages at the time of using it:
  • It works directly with the database which highly degrees the test's performance.
  • It is hard to maintain, as it is used by all of your specs (for the description's block that invokes it), and for that: a single change may trigger many errors among different tests.
  • It is very implementation dependant, it strongly relies on the model and the current specification of the database's schema.
To see the fixtures applied in a practical example, suppose we want to test a chat's application that displays some messages. For this functionality we write the following spec:

#spec/views/chats/show.html.erb_spec.rb
describe "views/chats/show.html.erb" do
fixture :chats, :messages

before(:each) do
@chat = chats(:myChat)
render "/chats/show.html.erb"
end

it "should render attributes in paragraph" do
response.should have_text(/MyString/)
end

it "should render all messages for the chat" do
response.should have_tag("tr>td", "Hello!", 1)
response.should have_tag("tr>td", "How are you?", 1)
response.should have_tag("tr>td", "Excellent!", 1)
end

end


With these fixtures:

#spec/fixtures/chats.yaml
myChat:
name: MyString
id: 1

#spec/fixtures/messages.yaml
one:
data: Hello
chat_id: 1
version: 1
user_id: 1

two:
data: How are you?
chat_id: 1
version: 2
user_id: 1
three:
data: Excellent!
chat_id: 1
version: 3
user_id: 1

Mocks and stubs way

The other option is to use mocks and stubs. Although there is a lot of information in the web about it, to help our discussion, I will describe it very shortly. From my experience & research, I understand that the main difference between those two is the following:

Stubbing a method is all about replacing the method with code that returns a specified result (or perhaps raises a specified exception). Mocking a method is all about asserting that a method has been called (perhaps with particular parameters).

As you can see, stubbing lets you define methods that are not currently implemented or we decided not to depend on. On the other hand, as we mock objects we specify the behavior that we would expect on the mocked object, making our test more Behavior Driven Development.

This is the recommended way of using Rspec as it defines the way the objects must collaborate to accomplish a certain task.

Here we show how we implement the chat's example using mocks.

#spec/views/chat/show.html.erb_spec.rb
describe "/chats/show.html.erb" do
include ChatsHelper

before(:each) do
@chat = mock_model(Chat)
@chat.stub!(:name).and_return("MyString")
@chat.stub!(:id).and_return(1)
@chat.stub!(:to_param).and_return(1)
message_1 = mock_model(Message)
message_1.stub!(:data).and_return("Hello!")
message_1.stub!(:version).and_return(1)
message_2 = mock_model(Message)
message_2.stub!(:data).and_return("How are you?")
message_2.stub!(:version).and_return(2)
message_3 = mock_model(Message)
message_3.stub!(:data).and_return("Excellent!")
message_3.stub!(:version).and_return(3)
@messages = [message_1, message_2, message_3]
@chat.stub!(:messages).and_return(@messages)
@message.stub!(:data)
assigns[:chat] = @chat
render "/chats/show.html.erb"
end

it "should render attributes in paragraph" do
response.should have_text(/MyString/)
end

it "should render all messages for the chat" do
response.should have_tag("tr>td", "Hello!", 1)
response.should have_tag("tr>td", "How are you?", 1)
response.should have_tag("tr>td", "Excellent!", 1)
end

end

As you can see, the idea behind mocks is to define some behavior that should be part of certain task, and it is very human readable as it uses many features of Rspec mocks.

Conclusions

As we did with fixtures, we have to mention that Mocking has some drawbacks compared with fixtures, such as: it tights the tests with the data they need and forces us to write, perhaps, to much testing code.

Despite this, I prefer Mocks/Stubs because I think they provide a very elegant way to work with BDD. I think this way because I find in BDD a solid foundation to agile development, then I choose to prioritize it over the other alternatives. Please, make a comment debate this interesting subject that interests me so much ;)

4 comentarios:

Anónimo dijo...

You should have use "fixtures" instead of "fixture" to import the fixtures. At least for me, with rspec 2 and rails 3 worked that way.

Anónimo dijo...

Also the "yaml" fixture file extension didn't work for me on Rails 2.3.8. I managed to load the fixtures after renaming the files to '*.yml'

iPad Application Development dijo...

I’ll immediately grab your rss feed as I can’t find your e-mail subscription link or newsletter service. Do you have any? Please let me know in order that I could subscribe. Thanks.

Kevin dijo...

I get the following error:
Mock 'User_1' received unexpected message :show_all_group with (no args)

I have searched through Google but none solution worked.