Rspec rails-controller-testing gem's assigns doesn't work - ruby-on-rails

I'm using Rails 5.2 with rspec-rails 3.7 and rails-controller-testing gems.
I have a controller that filters results on the index action (yeah, bad practice, legacy code, etc). The problem is I need to test that when I do GET work_orders/index params: { keywords: 'some word' }, the instance variable #work_orders returns filtered results, but if I use assigns(:work_orders) it returns nil. I even tested this assigning the last WorkOrder to that variable, but it still doesn't show the variable in the hash.
work_orders_controller.rb
def index
... some code ...
#work_orders = WorkOrder.last
respond_to do |format|
format.html
end
end
spec/controllers/work_orders_controller_spec.rb
require 'rails_helper'
describe WorkOrdersController do
describe 'GET index' do
it 'collects work orders filtered by courier_ot in #work_orders' do
get :index
expect(assigns(:work_orders)).to be_an_instance_of WorkOrder
end
end
end
That is the simplest example I tested and it doesn't work. The assigns call returns nil, and if I use byebug to inspect the hash, it only has this: {"marked_for_same_origin_verification"=>true}.
The real test should use get :index, params: { keywords: 'asdf' } and test that it gets the filtered work orders.
Any help would be appreciated. Thanks.

Related

When testing a method call that I know happens , I get a failure error stating the method was called 0 times

I have a controller that calls the CSVCreator class when a get request is made. People is just a list of Persons that have some basic values like name and title.
def index
respond_to do |format|
format.html
format.csv { send_data CSVCreator.new(people).create }
end
My CSVCreator looks like this
class CSVCreator
HEADERS = ["Name", "Date", "Title"].freeze
def initialize(people)
#people = people
end
def generate
CSV.generate do |csv|
csv << HEADERS
#people.each do |person|
row = [person.name, person.date, person.title]
csv << row
end
end
end
end
I'm trying to figure out how I would go about testing this in Rspec? What I've tried is
it "calls CSVCreator when get request is made" do
csv_creator = double("CSVCreator")
people = double("People")
allow(csv_creator).to receive(:new).with(people)
allow(csv_creator).to receive(:create)
expect(csv_creator).to receive(:new).with(people)
expect(csv_creator).to receive(:create)
get :index, format: :csv
end
My thought process was to decouple the controller and the CSVCreator and People classes. So to test the controller, I wanted to see if it correctly calls the methods it needs to, so I created test doubles for those objects. I'm new to RSpec and testing in general, so please let me know if my approach was incorrect.
My issue is, I get a failure saying
Failure/Error: expect(csv_creator).to receive(:new).with(people)
(Double "CSVCreator").new(#<Double "People">)
expected: 1 time with arguments: (#<Double "People">)
received: 0 times
I know my class works and it creates the CSV, and that CSVCreator.new(people).create being called in the controller. So I'm curious as to why I'm receiving this failure.
Create a class method on your CSVCreator that creates an instance and calls the generate method:
class CSVCreator
# ...
def self.perform(people)
new(people).generate
end
end
def index
respond_to do |format|
format.html
format.csv { send_data CSVCreator.perform(people) }
end
end
This creates a better API so that consumers don't have to have as much knowledge about the class and it makes it way easier to set expectations:
it "calls CSVCreator when get request is made" do
expect(CSVCreator).to receive(:perform)
get :index, format: :csv
end
However stubbing in people is a bit more problematic and depends on how its actually implemented. If its a method on the controller it would be hard to stub without using any_instance which is a code smell.
i guess that the input param people is created somewhere in your controller, but i'm sure it's not your double "People", you have to stub the method :new of the People class to return an instance_double of "People", Or in case you want to make sure that CSVCreator is created with a special "People", for example in your controller you call this to get people: PeopleService.some_logic(..) -> a_special_people, then stub that method allow(PeopleService).to receive(:some_logic).and_return(instance_double_people)
beside that, you could also create an instance_double of CSVCreator instead, then stub method :new of CSVCreator to return that instance_double, now you only need to verify that instance_double has called :create method since it makes sure that CSVCreator has called :new and the new instance of CSVCreator has called :create.
it "calls CSVCreator when get request is made" do
csv_creator = instance_double(CSVCreator)
people = instance_double(People)
allow(People).to receive(:new).and_return(people)
# allow(PeopleService).to receive(:some_logic).and_return(people)
allow(CSVCreator).to receive(:new).with(people).and_return(csv_creator)
allow(csv_creator).to receive(:create)
get :index, format: :csv
expect(csv_creator).to have_received(:create)
end

`flash.discard` in Rails 2.3 fails because flash is a Hash, not a FlashHash

Given a functional test such as:
def test_exciting_rails_upgrades
login(m=users(:manager))
post :import_users_map_fields, :csv_file => fixture_file_upload('/users.csv', 'text/csv')
assert flash.empty?
flash.discard
# other goodies omitted
end
In Rails 2.3.2 there are no errors, however in 2.3.15 the error is:
NoMethodError: undefined method `discard' for {}:Hash
/test/functional/broken_upgrades.rb:119:in `test_exciting_rails_upgrades'
Why is flash a Hash class instead of a FlashHash?
From the source it looks like both 2.3.2 and 2.3.15 ActionPack files lib/action_controller/flash.rb create the FlashHash class and inherit from Hash. However what is shown in this functional test in both 2.3.2 and 2.3.15 is a Hash class, not a HashFlash, so one cannot call discard on it.
Can anyone else reproduce this error with 2.3.15 and flash.discard?
Here are two test cases you can use to prove ActionController changes the type of 'flash' depending on whether or not it is already set.
In my app, you cannot see :index unless you're logged in, so in test_flash_is_now_a_flashhash you see that flash was set by the backend properly, while in test_flash_is_a_plain_hash it was not.
def test_flash_is_a_plain_hash
login(users(:permitted_user))
get :index
assert flash.instance_of?(Hash)
end
def test_flash_is_now_a_flashhash
get :index
assert_redirected_to :controller => "login"
assert flash.instance_of?(ActionController::Flash::FlashHash)
end
You can see this for yourself in the ActionController::TestRequest code:
def flash
session['flash'] || {}
end
Update: This has been fixed in Rails branch 2-3-stable.

respond_with won't work on nested routes

I've this code that I'm trying to get working.
class CommitRequestsController < ApplicationController
respond_to :json
def create
#commit_request = CommitRequest.new(params[:commit_request])
respond_with(repository, #commit_request)
end
private
def repository
Repository.find(params[:repository_id])
end
end
I also have this spec
CommitRequest.any_instance.stubs(:valid?).returns(false)
post(:create, {
format: "json",
repository_id: repository.id,
commit_request: {}
})
response.status.should_not eq(201)
The problem is that the spec is always failing.
It returns 201, even tho the created object isn't valid.
Removing the mock line results in the same problem,
even tho the created object is invalid (for real this time).
I'm using Rails 3.2.

Mocha not mocking a class method in a functional test (Rails 3)

In a rails 3 app, I'm using mocha to do some mocking in my functional tests. However it doesn't seem to mock a class method in the functional controller.
Controller code
class TagsController < ApplicationController
respond_to :json
def index
response = User.tags_starting_with(params[:query])
respond_with response
end
end
Functional test
class TagsControllerTest < ActionController::TestCase
context "index action with query" do
setup do
query = "A_QUERY"
get :index, :query => query, :format => "json"
#tags = ["these", "are", "test", "tags"]
User.expects(:tags_starting_with).returns(#tags).once
end
should "return JSON formatted tags array" do
tags = JSON::parse #response.body
assert_equal #tags, tags
end
end
end
Gemfile
gem "mocha"
If I run this test, I keep running into
- expected exactly once, not yet invoked: User.tags_starting_with(any_parameters)
If I use rails console test I can mock a class method just fine, and it works as expected.
I've been through this post and have done the Gemfile, require "false" bit. But to no avail, it just doesn't want to mock the class method of the User in the controller.
Other things I've tried, if I do User.tags_starting_with("bla") in the test itself, the expectation passes.
So any ideas on why the User in the controller isn't being mocked correctly?
As said on Twitter:
You're setting you your mock after you're doing your request :-)

Understanding Rails 3's respond_with

Utilizing ActionController's new respond_with method...how does it determine what to render when action (save) is successful and when it's not?
I ask because I'm trying to get a scaffold generated spec (included below) to pass, if only so that I can understand it. The app is working fine but, oddly, it appears to be rendering /carriers (at least that's what the browser's URL says) when a validation fails. Yet, the spec is expecting "new" (and so am I, for that matter) but instead is receiving <"">. If I change the spec to expect "" it still fails.
When it renders /carriers that page shows the error_messages next to the fields that failed validation as one would expect.
Can anyone familiar with respond_with see what's happening here?
#carrier.rb
validates :name, :presence => true
#carriers_controller.rb
class CarriersController < ApplicationController
respond_to :html, :json
...
def new
respond_with(#carrier = Carrier.new)
end
def create
#carrier = Carrier.new(params[:carrier])
flash[:success] = 'Carrier was successfully created.' if #carrier.save
respond_with(#carrier)
end
Spec that's failing:
#carriers_controller_spec.rb
require 'spec_helper'
describe CarriersController do
def mock_carrier(stubs={})
(#mock_carrier ||= mock_model(Carrier).as_null_object).tap do |carrier|
carrier.stub(stubs) unless stubs.empty?
end
end
describe "POST create" do
describe "with invalid params" do
it "re-renders the 'new' template" do
Carrier.stub(:new) { mock_carrier(:save => false) }
post :create, :carrier => {}
response.should render_template("new")
end
end
end
end
with this error:
1) CarriersController POST create with invalid params re-renders the 'new' template
Failure/Error: response.should render_template("new")
expecting <"new"> but rendering with <"">.
Expected block to return true value.
# (eval):2:in `assert_block'
# ./spec/controllers/carriers_controller_spec.rb:81:in `block (4 levels) in <top (required)>'
tl:dr
Add an error hash to the mock:
Carrier.stub(:new) { mock_carrier(:save => false,
:errors => { :anything => "any value (even nil)" })}
This will trigger the desired behavior in respond_with.
What is going on here
Add this after the post :create
response.code.should == "200"
It fails with expected: "200", got: "302". So it is redirecting instead of rendering the new template when it shouldn't. Where is it going? Give it a path we know will fail:
response.should redirect_to("/")
Now it fails with Expected response to be a redirect to <http://test.host/> but was a redirect to <http://test.host/carriers/1001>
The spec is supposed to pass by rendering the new template, which is the normal course of events after the save on the mock Carrier object returns false. Instead respond_with ends up redirecting to show_carrier_path. Which is just plain wrong. But why?
After some digging in the source code, it seems that the controller tries to render 'carriers/create'. There is no such template, so an exception is raised. The rescue block determines the request is a POST and there is nothing in the error hash, upon which the controller redirects to the default resource, which is the mock Carrier.
That is puzzling, since the controller should not assume there is a valid model instance. This is a create after all. At this point I can only surmise that the test environment is somehow taking shortcuts.
So the workaround is to provide a fake error hash. Normally something would be in the hash after save fails, so that kinda makes sense.

Resources