views:

409

answers:

3

I've run into this problem with testing. Let's assume I have two models, User and Post, where user has_many :posts.

I'm trying to spec out a code block that includes something like this:

user = User.find(123)
post = user.posts.find(456)

I know how to mock out the User.find and user.posts parts. The user.posts mock returns an array of Post objects. And when it get's to .find(456) part, everything breaks down with no block given exception.

So my question here is: what do I return as the result of the user.posts mock, so that .find(456) method works on it? User.first.posts.class says it's Array, but obviously there's something more that makes the AR-style find calls work. I'm not overjoyed by the prospect of mocking out find method on the returned object.

PS Before you suggest the obvious and good answer of stop mocking about and using fixtures/seeding the test database with necessary data, here's the catch: legacy scheme. Both User and Post work on top of database views not tables, and changing it so that they are tables in test database seems wrong to me.

+3  A: 

The issue is that user.posts isn't actually a simple Array; it's an association proxy object. The way to stub it is probably something like this (though the exact syntax depends on which mocking framework you're using):

def setup
  @user = mock(User)
  User.stub(:find).with(123).return(@user)
  user_posts = mock(Object)
  @user.stub(:posts).return(user_posts)
  @post = mock(Post)
  user_posts.stub(:find).with(456).return(@post)
end

Then in your test, User.find(123) will return @user and @user.posts.find(456) will return @post. If you need @user.posts to act like more of the Array in your tests you can create a mock(Array) and stub the [](index) method.

James A. Rosen
That is the 'ugly' way that I didn't want to do, but your mention of 'association proxy object' provided me with the keywords I was missing, and searching for it led to what I was looking for, thanks!
Toms Mikoss
@Toms, If your solution is unique among these answers, perhaps you could add add it as another answer. I'd like to see how you solved it.
Wayne Conrad
The ugly way, mocks upon mocks. I was just happy figuring this out - wasn't the first time I encountered this situation, and it was bothering me.
Toms Mikoss
I share your pain. When the tests start to look like OO spaghetti, you start to think there must be a better way.
Wayne Conrad
If your tests are starting to get ugly, it might be time to refactor some code. One common refactoring I find useful (not in this particular case) is to wrap a series of nested scopes in one Model-Class method.
James A. Rosen
+1  A: 

This is the classic Law of Demeter problem. There is an excellent article about it and how to solve it in a more elegant way here:

http://lukeredpath.co.uk/blog/demeters-revenge.html

Hope that helps.

emson
A: 

You could look into the stub_chain method offered by RSpec.

http://apidock.com/rspec/Spec/Mocks/Methods/stub_chain#855-stub-chain-is-very-useful-when-testing-controller-code

rjsvaljean