Understanding RSpec 2's generated specs - ruby-on-rails

I think I may be biting off more than I can chew by trying to learn RSpec on my own...there doesn't appear to be any complete documentation on RSpec 2?!? at least none that I have been able to find...
Anyway, I, in a feeble attempt to integrate RSpec, started by examining the 29 specs created by the scaffold generator in Rails 3. Some I understand but much still escapes me. Hoping someone can help (or point me to decent RSpec 2 documentation).
For example in the first code block below (def mock_clown...) I can assume mock_clown is creating a mock of my Clown class to test against. But what exactly is going on? what is generating this "mock"? what does .as_null_object and .tap mean? This is very confusing as I can't find an example to compare this block to in any docs or tutorials I've come across...
#clowns_controller_spec.rb
require 'spec_helper'
describe ClownsController do
def mock_clown(stubs={})
(#mock_clown ||= mock_model(Clown).as_null_object).tap do |clown|
clown.stub(stubs) unless stubs.empty?
end
end
describe "GET index" do
it "assigns all clowns as #clowns" do
Clown.stub(:all) { [mock_clown] }
get :index
assigns(:clowns).should eq([mock_clown])
end
end
...
describe "POST create" do
...
describe "with invalid params" do
it "assigns a newly created but unsaved clown as #clown" do
Clown.stub(:new).with({'these' => 'params'}) { mock_clown(:save => false) }
post :create, :clown => {'these' => 'params'}
assigns(:clown).should be(mock_clown)
end
it "re-renders the 'new' template" do
Clown.stub(:new) { mock_clown(:save => false) }
post :create, :clown => {}
response.should render_template("new")
end
end
...
end

The best source of documentation for rspec is probably its github wikis. Here's a general link to all gems:https://github.com/rspec/rspec/wiki. Also, checkout the rdoc, linked to here.
As for specific answers, as_null_object causes the mock to record and ignore all method calls. (This is great, as you don't have to spec out every single method on an object, just the ones you care about.)
Tap is a Ruby 1.9 feature. From the documentation at this link:
Object#tap
Passes the object to the block and returns it (meant to be used for call chaining).
For learning Rspec 2 Webrat and Cucumber on your own, I definitely recommend checking out the RSpec Book. Awesome resource, and covers all aspects.

Related

Assigns has been extracted to a gem. Use gem 'rails-controller-testing'. Is there an alternative for the gem?

I am writing some tests for a controller tasks, the index action, which has an instance variable #tasks with all the tasks (Task.all).
If I follow the official documentation:
RSpec.describe TeamsController do
describe "GET index" do
it "assigns #teams" do
team = Team.create
get :index
expect(assigns(:teams)).to eq([team])
end
it "renders the index template" do
get :index
expect(response).to render_template("index")
end
end
end
The assigns method is moved to the gem file 'rails-controller-testing'.
I have two questions:
1 - How can I achieve the same as expect(assigns(:teams)).to eq([team]). I guess I am asking, how can I check if I have an instance variable in the index action with values [team]
2 - If this method was moved to the gem, I read in the Github issues, that the reason is: You shouldn't test it there, controller should just test response, cookies etc. But I am confuse, since in relish you can test the instance variable. Should I test it there or not? If not, where? In my views/index_spec.rb, testing if I have all the teams?
3 - Alternative: Since TeamsController is a normal class, should I create a spec in the spec/models/folder spec/models/tasks_controller.rb and there test if the method index has the instance variable #teams with the content that I want?
Thanks
The whole idea is that instead of poking inside your controller and testing its internal variables is flawed you should instead test your controllers by testing the output.
In RSpec you can do this with request and feature specs.
# config/specs/features/teams_spec.html
RSpec.feature 'Teams' do
scenario 'when a user views the teams' do
Team.create(name: 'Team Rocket')
visit '/teams'
expect(page).to have_content 'Team Rocket'
end
end
# config/specs/requests/teams_spec.html
RSpec.describe 'Teams', type: :request do
describe 'GET /teams.json' do
it "includes the team" do
team = Team.create(name: 'Team Rocket')
get teams_path(format: :json)
expect(parsed_response.first['name']).to eq 'Team Rocket'
end
end
describe 'GET /teams' do
it "includes the team" do
team = Team.create(name: 'Team Rocket')
get teams_path
expect(page).to have_content 'Team Rocket'
end
end
end
The key difference is that feature specs test the app from a user story POV by driving a browser simulator while request specs are lighter weight and you just test against the raw response.
1 - How can I achieve the same as expect(assigns(:teams)).to
eq([team]). I guess I am asking, how can I check if I have an instance
variable in the index action with values [team]
Either use the assigns gem for legacy compatiblity or test the rendered output.
2 - If this method was moved to the gem, I read in the Github issues,
that the reason is: You shouldn't test it there, controller should
just test response, cookies etc. But I am confuse, since in relish you
can test the instance variable. Should I test it there or not? If not,
where? In my views/index_spec.rb, testing if I have all the teams?
If by Relish you mean RSpec, then its been taking a while for RSpec-rails to catch up to the state-of-art in Rails testing. But the same still applies. The offical recommendation of the RSpec team is to not use assigns and faze out controller specs in favor of request specs. View specs are not really relevant here - they are used if you want to test complex views in isolation.
3 - Alternative: Since TeamsController is a normal class, should I
create a spec in the spec/models/folder
spec/models/tasks_controller.rb and there test if the method index has
the instance variable #teams with the content that I want?
Just no. Controllers are not just normal classes. You can't just instantiate a controller with MyController.new, thats why controller tests have all that stubbing in place.

Rails 5 Rspec receive with ActionController::Params

I have just upgraded to Rails 5. In my specs I have the following
expect(model).to receive(:update).with(foo: 'bar')
But, since params no longer extends Hash but is now ActionController::Parameters the specs are failing because with() is expecting a hash but it is actually ActionController::Parameters
Is there a better way of doing the same thing in Rspec such as a different method with_hash?
I can get around the issue using
expect(model).to receive(:update).with(hash_including(foo: 'bar'))
But that is just checking if the params includes that hash, not checking for an exact match.
You could do:
params = ActionController::Parameters.new(foo: 'bar')
expect(model).to receive(:update).with(params)
However it still smells - you should be testing the behaviour of the application - not how it does its job.
expect {
patch model_path(model), params: { foo: 'bar' }
model.reload
}.to change(model, :foo).to('bar')
This is how I would test the integration of a controller:
require 'rails_helper'
RSpec.describe "Things", type: :request do
describe "PATCH /things/:id" do
let!(:thing) { create(:thing) }
let(:action) do
patch things_path(thing), params: { thing: attributes }
end
context "with invalid params" do
let(:attributes) { { name: '' } }
it "does not alter the thing" do
expect do
action
thing.reload
end.to_not change(thing, :name)
expect(response).to have_status :bad_entity
end
end
context "with valid params" do
let(:attributes) { { name: 'Foo' } }
it "updates the thing" do
expect do
action
thing.reload
end.to change(thing, :name).to('Foo')
expect(response).to be_successful
end
end
end
end
Is touching the database in a spec inheritenly bad?
No. When you are testing something like a controller the most accurate way to test it is by driving the full stack. If we in this case had stubbed out #thing.update we could have missed for example that the database driver threw an error because we where using the wrong SQL syntax.
If you are for example testing scopes on a model then a spec that stubs out the DB will give you little to no value.
Stubbing may give you a fast test suite that is extremely brittle due to tight coupling and that lets plenty of bugs slip through the cracks.
I handled this by creating in spec/rails_helper.rb
def strong_params(wimpy_params)
ActionController::Parameters.new(wimpy_params).permit!
end
and then in a specific test, you can say:
expect(model).to receive(:update).with(strong_params foo: 'bar')
It's not much different from what you're already doing, but it makes the awkward necessity of that extra call a little more semantically meaningful.
#max had good suggestions about how to avoid this altogether, and I agree they switched away from a hash to discourage using them with hashes interchangeably.
However, if you still want to use them, as a simple hack for more complex situations (for instance if you expect using a a_hash_including), you can try using something like this:
.with( an_object_satisfying { |o|
o.slice(some_params) == ActionController::Parameters.new(some_params)
})

Rails RSpec: Controller Testing, checking if errors Array of model is filled with entries if new record cannot be created due to validation error

I have a still pretty simple Rails application that I want to develop using BDD with Cucumber and TDD with RSpec. Currently, I am hanging at a test where I want to check that if a new instance of an Organizer (that's the model I have) cannot be created due to a validation error. I would like to check that the errors Array of the object to be created is not empty so that I can be sure that error messages are available for showing them in the view.
require 'spec_helper'
describe OrganizersController do
render_views
describe "POST 'create'" do
describe "with invalid arguments" do
before(:each) do
request.env["HTTP_REFERER"] = organizers_new_path
#organizer_args = { :name => "" }
end
it "should return a non-empty list of errors" do
post 'create', :organizer => #organizer_args
#organizer.errors.empty?.should_not be_true
end
end
end
end
I am developing based on Rails 3.2.9 with RSpec 2 and cucumber-rails.
Any suggestions are appreciated. Thanks!
You should use assigns method to get instance variable from controller action:
assigns(:organizer).errors.empty?.should_not be_true
The latest preferred syntax is:
expect(assigns(:organizer).errors.empty?).to_not be_true
thanks for the answer guys but I'd like to suggest a slightly nicer syntax:
expect(assigns(:organizer).errors).to_not be_empty
(unrelated to the question 👇)
Basically whenever you have a method that ends with ? you'll have the corresponding rspec matcher that starts with be_ e.g.
1.odd? #=> true
expect(1).to be_odd

last_response.body is empty in Rack::Test rspec test (but not in actual app)

I've ben struggling with this for a couple hours now. I am using Rack::Test to write API tests for my Rails 3.2 app.
Regardless of what I do, last_response has an empty body (well, specifically it has "{}", so 2 characters).
Here are the tests:
describe "updating a product set with JSON" do
def app
ProductSetsController.action(:update)
end
let(:update_json) { ... }
before do
#product_set = FactoryGirl.build(:product_set)
end
it { #failures.should == 0 }
it "should not increment the product set count" do
expect { put :update, update_json }.to_not change(ProductSet, :count).by(1)
end
it "should increment the conditions count" do
expect { put :update, update_json }.to change(#product_set.conditions, :count).by(2)
end
context "response should be valid" do
before do
put :update, update_json
end
subject { last_response }
it { should be_ok }
end
end
All these tests pass. But the body is empty.
The weird thing is that if I run the actual application the response body is definitely not empty. It has JSON about the updated product_set.
So I'm setting up the test incorrectly somehow. I bet I'm overlooking something really silly as this is my first time using Rack::Test. Any thoughts?
Just had a thought that I may not be setting the request headers correctly. I'm also using RABL for generation.
UPDATE:
The problem is indeed with RABL. Haven't figured out a solution yet, but if I use:
respond_with(#product_set.update_attributes(get_properties(params)),
file: 'app/views/product_sets/update.json.rabl', :handlers => [:rabl])
instead of:
respond_with(#product_set.update_attributes(get_properties(params)))
then it works in testing, whereas both work in development or production. Also I've confirmed that it's not a Gemfile problem.
Found the answer. In short, make sure to use the keyword "render_views" at the top of the root describe block in the rspec document to ensure that views are rendered correctly.
This was the helpful article:
https://github.com/nesquena/rabl/wiki/Testing-with-rspec

separate helper function to log in before every rspec request test is run

I've been struggling with creating a login function that should be executed before any rspec test is run.
What I have right now is:
def login
post "/session", { "session[username]" => "bjones" }
end
in my spec_helper.rb file
Then, I have the following in one of my spec.rb files in the requests directory.
require 'spec_helper'
describe "Sessions" do
describe "GET /dashboards" do
login
it "displays the dashboard" do
get dashboard_path
puts response.body
end
end
end
However, when I try running the test, I get:
undefined method `post' for #<Class:0x4f08828> (NoMethodError)
I'm pretty new to rails and completely new to testing and rspec so maybe there's something fundamental I'm missing here. Basically, all I want to do is set that session variable so that when the test is run I will be logged in. Perhaps a different approach would be better? Or maybe I need to put that login function in a different place?
I came across this answer which was sort of useful but it's not for rspec so I'm having trouble understanding where such a helper function would go.
Try
let(:login) {
post "/session", { "username" => "bjones" }.to_json
}
This might have to be revised to use .to_json or not, depending on what content type the controller accepts.

Resources