Mocking ActiveRecord relationship beheavior in RSpec tests - ruby

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.

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.

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
Update: Per ryan2johnson9 the updated documentation is : https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/working-with-legacy-code/message-chains

Related

Rspec Double leaking to another example

I am testing a class that makes use of a client that makes external requests and I would like to mock this client, but verify that it gets called, however I am getting a double error.
My test looks like something like this:
describe '#execute' do
let(:attributes) { {foo: 'bar'} }
let(:client_double) { double('client', create: nil) }
let(:use_case) { described.class.new }
before do
allow(Client::Base).to receive(:new).and_return(client_double)
use_case.execute(attributes)
end
it 'creates something' do
expect(Something.find_by(foo: 'bar')).not_to be_nil
end
it 'calls client' do
expect(client).to have_received(:create).with('bar')
end
end
and the first example passes as expected, however rspec keeps breaking in the second example giving me this error:
#<Double "foo"> was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.
someone knows what I can do to fix it?
Reusing Fixtures with Let Methods
In this case, before is actually before(:each), which is reusing the client_double and attributes you defined with the #let helper method. The let commands make those variables functionally equivalent to instance variables within the scope of the described object, so you aren't really testing freshly-created objects in each example.
Some alternatives include:
Refactor to place all your setup into before(:each) without the let statements.
Make your tests DAMP by doing more setup within each example.
Set up new scope for a new #describe, so your test doubles/values aren't being reused.
Use your :before, :after, or :around blocks to reset state between tests, if needed.
Since you don't show the actual class or real code under test, it's hard to offer specific insights into the right way to test the object you're trying to test. It's not even clear why you feel you need to test the collaborator object within a unit test, so you might want to give some thought to that as well.
It turns out I was using a singleton as a client and haven't realized before, so it was trully class caching it through examples. To fix it all I did was mock the instantiate method instead of the new method and everything worked.
So in the end this worked:
allow(Client::Base).to receive(:instantiate).and_return(client_double)

Ruby rspec mocking a class with an attribute that is a hash

I have a class I am testing, call it myfoo. It accesses a class called yourbar. Specifically something like this...
yourbar_obj.projects[project_name]
In my spec code I have this
let(:yourbar_obj) { Class.new }
and I want to mock it to respond to the hash attribute access.
So I tried this
expect(yourbar_obj).to receive(projects).and_return(some_obj)
But when I run the code it says
NoMethodError: undefined method `projects' ...
Is it possible to mock a hash access like that? The same type of thing works for regular method calls.
I even tried adding a .with(project_name) just in case. Same error.
thoughts?
Thanks to Max's help. Here is the correct answer...
some_hash_obj[project_name] = some_obj
expect(yourbar_obj).to receive(:projects).and_return(some_hash_obj)
Two key parts. The : before projects, and some_hash_obj must be a hash. I was trying to return the value (which was an obj) at the hash index in one shot, but that ain't how it works. return the hash, and the [] will apply to it.

How to stub method with specific parameter (and leave calls with other parameters unstubbed) in Mocha?

This question may seem like a duplicate of this one but the accepted answer does not help with my problem.
Context
Since Rails 5 no longer supports directly manipulating sessions in controller tests (which now inherit from ActionDispatch::IntegrationTest), I am going down the dark path of mocking and stubbing.
I know that this is bad practice and there are better ways to test a controller (and I do understand their move to integration tests) but I don't want to run a full integration test and call multiple actions in a single test just to set a specific session variable.
Scenario
Mocking/stubbing a session variable is actually quite easy with Mocha:
ActionDispatch::Request::Session.any_instance.stubs(:[]).with(:some_variable).returns("some value")
Problem is, Rails stores a lot of things inside the session (just do a session.inspect anywhere in one of your views) and stubbing the :[] method obviously prevents access to any of them (so session[:some_other_variable] in a test will no longer work).
The question
Is there a way to stub/mock the :[] method only when called with a specific parameter and leave all other calls unstubbed?
I would have hoped for something like
ActionDispatch::Request::Session.any_instance.stubs(:[]).with(:some_variable).returns("some value")
ActionDispatch::Request::Session.any_instance.stubs(:[]).with(anything).returns(original_value)
but I could not find a way to get it done.
By what I see, this is a feature not available in mocha
https://github.com/freerange/mocha/issues/334
I know this does exist in rspec-mock
https://github.com/rspec/rspec-mocks/blob/97c972be57f2c060a4a7fb8a3c5700a5ede693f0/spec/rspec/mocks/stub_implementation_spec.rb#L29
One hacky way that you an do it though, is to store the original session in an object, then mock that whenever a controller receives session, it returns another mock object, and in this you may either return a mocked velue, or delegate the call to the original session
class MySession
def initialize(original)
#original = original
end
def [](key)
if key == :mocked_key
2
else
original[key]
end
end
end
let!(original_session) { controller.send(:session) }
let(:my_session) { MySession.new(original_session) }
before do
controller.stubs(:session) { my_session }
end
Guess that mocha also allows you to do block mocking, so you don't need the class, but you need that original_session to be called
But I don't see a clean way

Caching ActiveRecord model instance methods

Say I have a user model. It has an instance method called status. Status is not an association. It doesn't follow any active record pattern because it's a database already in production.
class User < ActiveRecord::Base
def status
Connection.where(machine_user_id: self.id).last
end
end
So I do this.
#users = User.all
First of all I can't eager load the status method.
#users.includes(:status).load
Second of all I can't cache that method within the array of users.
Rails.cache.write("user", #users)
The status method never gets called until the view layer it seems like.
What is the recommended way of caching this method.
Maybe this instance method is not what I want to do. I've looked at scope but it doesn't look like what I want to do.
Maybe I just need an association? Then I get the includes and I can cache.
But can associations handle complex logic. In this case the instance method is a simple query. What if I have complex logic in that instance method?
Thanks for any help.
Have You tried to encapsulate this logic inside some plain Ruby object like this (I wouldn't use this for very large sets though):
class UserStatuses
def self.users_and_statuses
Rails.cache.fetch "users_statuses", :expires_in => 30.minutes do
User.all.inject({}) {|hsh, u| hsh[u.id] = u.status; hsh }
end
end
end
After that You can use some helper method to access cached version
class User < ActiverRecord::Base
def cached_status
UserStatuses.users_and_statuses[id]
end
end
It doesn't solve Your eager loading problem, Rails doesn't have any cache warming up techniques built in. But by extracting like this, it's easily done by running rake task in Cron.
Also in this case I don't see any problems with using association. Rails associations allows You to submit different options including foreign and primary keys.

Retaining Form data after POST in Ruby on Rails

I need to retain the Form data submitted in one view to be used in another view.
I'll be using POST method to submit the data. Is there anyway I can retrieve data from the POST method in Ruby, like in PHP I would use $title=$_POST["title"].
Any ideas?
Thanks and Cheers !
I think you just want the params hash? rubyonrails.org is down at the moment, but when it's back up take a read of the Action Controller Overview (or go to the google cache):
"Rails does not make any distinction between query string parameters and POST parameters, and both are available in the params hash in your controller"
For this, you need to understand the rationale behind the MVC pattern. Depending on whether or not you want to persist your data in the database, you derive your model-class from ActiveRecord, but as persistence seems not be context of your question, here is what you could try:
First, define a model like this
class Foo
# define variables here
attr_accessor :param1 # create reader and writer methods for param1
end
In your controller action:
def action1
#foo = new Foo
# pass parameters by using the params[] hash, e.g.
#foo.param1 = params[:param1]
end
You can then access the #foo object from every other view in your controller.
BTW, just found this screencast around the topic, #193 from railscasts.

Resources