ActiveRecord, Equality, and Rogue

Almost done with my model specs! I ran into a couple of interesting problems that I posted on Stack Overflow, one of which I’ll cover briefly below. Oh yeah, and Rogue! I’ll explain that in the last section of this post.

The problem I ran into (from my StackOverflow post)

If you just read the whole post, skip to the last section of this blog post.

I’m testing chats between users in my app. I’m using RSpec and FactoryGirl

The test that’s not passing:

chat_spec.rb
1
2
3
4
5
6
it "creates a chat if one does not exist" do
  bob = create(:user, username: "bob")
  dan = create(:user, username: "dan")
  new_chat = Chat.create(user_id: dan.id, chatted_user_id: bob.id)
  expect(Chat.where("chatted_user_id = ?", bob.id).first).to equal(new_chat)
end

The failure message says:

1
2
3
4
5
6
7
8
9
Failure/Error: expect(Chat.where("chatted_user_id = ?", bob.id).first).to equal(new_chat)

   expected #<Chat:70120833243920> => #<Chat id: 2, user_id: 2, chatted_user_id: 3>
        got #<Chat:70120833276240> => #<Chat id: 2, user_id: 2, chatted_user_id: 3>

   Compared using equal?, which compares object identity,
   but expected and actual are not the same object. Use
   `expect(actual).to eq(expected)` if you don't care about
   object identity in this example.

Why is my query returning a different object id?

Answer from Simone Carletti on StackOverflow

equal checks object identity. The objects you are testing are two objects (instances) referencing the same record, but they are actually different objects from a Ruby virtual machine point of view.

You should use

1
expect(Chat.where("chatted_user_id = ?", bob.id).first).to eq(new_chat)

To better understand the problem, look at the following example

1
2
3
4
2.0.0-p353 :001 > "foo".object_id
 => 70117320944040
2.0.0-p353 :002 > "foo".object_id
 => 70117320962820

Here I’m creating two identical strings. They are identical, but not equal because they are actually two different objects.

1
2
3
4
2.0.0-p353 :008 > "foo" == "foo"
 => true
2.0.0-p353 :009 > "foo".equal? "foo"
 => false

That’s the same issue affecting your test. equal checks if two objects are actually the same at the object_id level. But what you really want to know is if they are the same record.

So what’s happening under the hood?

The “where” query returns an array of objects matching the query, and those objects must be Active Record objects that just handle the data. If I make another query, Active Record must be creating another set of objects to handle each row of data.

I wanted to confirm this before throwing it out on the web as “fact”, so here’s what an Active Record object is from Martin Fowler himself:

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.

Think of an Active Record object sort of like Rogue (see above). For those of you who don’t remember, Rogue absorbs the memories and abilities of any person she touches.

Similarly (sorta), an Active Record objects takes the data (memories) of a certain row and then takes on the behavior (abilities) of the object type matching the table name.

Ok, so that was really just an excuse to get Rogue somewhere on my blog. While I’m at it, here’s one of her destroying Ororo (Storm).

So there you have it! Moral of the story: generally remember to use eq to test equivalence in RSpec with ActiveRecord.

Oh, and here’s a bonus resource: equal, eql, eq, and == in RSpec.

Comments