0

I have an action in my controller that loads up an instance variable with an instance of an object from an API I have no control over:

def index
  @obj = MyObj.find(id: params[:id])
end

MyObj.find makes an API call and returns me what that API returns.

Now say I want to write a test for my view, but I can't use a test database since my app is dependant on that API. I can never count on the API returning me a testable object and some of the tests I want to try are dependant on the state of that object.

I would like to be able to, before rendering my view, create my own test @obj manually and have my tests work on a view render informed by that @obj.

Ideally it would be something like this:

before(:all) do
  @obj = {attr1: "abc", attr2: 123}
  driver.navigate.to("#{ENV['RAILS_HOST']}/my_view/123")
end

Which clearly doesn't work. Is there some way to do this?

EDIT: Trying to stub the method doesn't seem to work, here's how it currently looks:

Spec:

before(:each) do
  allow(Library::Equipment)
    .to receive(:mymethod)
    .with(123)
    .and_return("Stub method")

  puts Library::Equipment.mymethod(123)
  #prints "Stub method"
  driver.navigate.to("#{ENV['RAILS_HOST']}/library/equipments/123/variables")
end

/library/equipments/:equipment_id/variables routes to library/variables#index, which looks like this:

def index
  @test = Library::Equipment.mymethod(123)
  puts @test
  #prints "Real method"
  # other code...
end

My Library::Equipment class has this class method:

def self.mymethod(param)
  "Real method"
end

And within my index.html.erb I simply have <%= @test %> to see what it contains. As you can see, the return of mymethod differs when called from my spec file and from my index action

2
  • Using RSpec::Mocks.with_temporary_scope will allow you to use stubbing in a before(all) but the stubbing will be removed when the scope ends. Please remove the with_temporary_scope AND change before(:all) to before(:each)/before at the same time. Commented Oct 5, 2017 at 12:51
  • The results are the same, I have edited the question to reflect the changes made. Commented Oct 5, 2017 at 13:25

1 Answer 1

1

You can stub the call to MyObj.find via rspec's allow method:

let(:obj) { { attr1: "abc", attr2: 123 } }
let(:obj_id) { 123 }

before(:each) do
  allow(MyObj)
    .to receive(:find)
    .with(id: obj_id)
    .and_return(obj)

  driver.navigate.to("#{ENV['RAILS_HOST']}/my_view/123")
end

allow will offer you the possibility to mock the response to a method call (e.g find) and return an answer of your choosing. That way, when your controller makes a call to MyObj.find, the actual implementation of find is not called, but rather rspec code which returns the object specified via and_return. The with method is only there to narrow down the mock so that the mock will not respond if the parameter doesn't match. It is probably optional in your case.

Please note that I altered the before(:all) to a before(:each). The former would execute only once, before the entire suite. As you want independent tests, you don't want one altering obj which would spill over to the next test. Also, it seems not to be possible to access let in a before(:all) block

Given that Selenium tests are typically acceptance tests, I would find such a mocking suspicious, however.

If you don't have access to an external API, providing canned responses might be irritating to work with but at least the layer that is stubbed is clear.

Sign up to request clarification or add additional context in comments.

7 Comments

I can't get that to work. I'm getting errors regarding "let declaration obj_id accessed in a before(:context) hook". Also, how exactly does that "allow" method work? Does it mean that any time I call MyObj.find(id: 123) it will return that specific object? This works within my controller?
@bpromas I elaborated the response to answer your questions.
This doesn't work. The find that runs within my controller's action (due to my driver.navigate.to call) runs normally and fails to find something with id = 123. However, if I call MyObj.find(id: 123) right under the allow method, it returns me { attr1: "abc", attr2: 123 }, the way it should. Two questions: 1) MyObj here is meant to be a class, correct? 2) In my case, MyObj doesn't have a find method, but includes one from a support model. Does this change things?
Yes it does change things. When mocking/stubbing you'll have to identify the place in the code where you want to make the cut between executed code and stubbed one. So you would have to adapt the mock definition (allow) to your actual code, this applies to the object the method is called on as well as the method itself and the parameters. And that is why using mocking has a smell to it when using it with Selenium: 1) no blackbox texting 2) rather brittle.
Yeah, it's not working. If I make up a new method that I know belongs to MyObj, the method I build in allow isn't the one that's called in my controller, only if I call it within my spec file can I get the return that I want from it. :/
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.