I have a rails controller that returns only json
def index
if params[:filtered] = 'someValue'
#json = Model.where(some_conditions).to_json
else
#json = Model.where(some_other_conditions).to_json
end
render json: #json
end
What is the correct way to test that the action is returning the expected #json objects?
I've tried the following
describe "GET #index" do
before :each do
get :index, filtered: 'someValue'
end
it { expect( response.body ).to eq 'my expected response' }
end
But I'm getting
Failure/Error: it { expect( response.body ).to eq 'my expected response' }
expected: 'my expected response'
got: "[]"
I'm having trouble determining whether there is a problem in the underlying controller, or whether I've simply written a bad test.
Is response.body the correct way to get the json payload?
Help appreciated!
Both your controller and spec are somewhat off.
You don't need to call to_json on the object that you want to render.
If you use the :json option, render will automatically call to_json
for you.
http://guides.rubyonrails.org/layouts_and_rendering.html
The reason your spec is giving you "[]" is that Model.where(some_conditions) is returning an empty collection. An empty collection renders as an empty array in JSON.
Either the scope does not work as intended or your test setup is flawed. Remember that let variables are lazy loading and you either need to use let! or reference the variable for records to be inserted into the test db.
# polyfill for Rails 4. Remove if you are using Rails 5.
let(:parsed_response) { response.body.to_json }
describe "GET #index" do
# or use fixtures / factories
let!(:model) { Model.create!(foo: 'bar') }
before :each do
get :index, filtered: 'someValue'
end
expect(parsed_response.first["id"].to_i).to eq model.id
end
Related
So I am having some trouble getting some RSpec tests to FAIL. No matter what I try they are always passing, well, one of the tests works correctly and I verified it can fail if I modify the code it is testing.
I am trying to test the content of JSON responses made to an external API to make sure my controller is sorting these return JSON objects correctly.
Am I missing something here? Why cant I get these to fail?
RSpec.describe 'Posts', type: :request do
describe 'ping' do
it 'returns status 200' do # This text block works correctly.
get "/api/ping"
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response).to have_http_status(200)
end
end
describe 'get /api/posts' do # all tests below always pass, no matter what.
it 'should return an error json if no tag is given' do
get "/api/posts"
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response.body).to eq("{\"error\":\"The tag parameter is required\"}")
end
it 'should return a json of posts' do
get "/api/posts?tags=tech" do
expect(body_as_json.keys).to match_array(["id", "authorid", "likes", "popularity", "reads", "tags"])
end
end
it 'should sort the posts by id, in ascending order when no sort order is specified' do
get "/api/posts?tags=tech" do
expect(JSON.parse(response.body['posts'][0]['id'].value)).to_be(1)
expect(JSON.parse(response.body['posts'][-1]['id'].value)).to_be(99)
end
end
it 'should sort the posts by id, in descending order when a descending order is specified' do
get "/api/posts?tags=tech&direction=desc" do
expect(JSON.parse(response.body['posts'][0]['id'].value)).to_be(99)
expect(JSON.parse(response.body['posts'][-1]['id'].value)).to_be(1)
end
end
end
Within the get block of the 'should return an error json if no tag is given' do block I even tried expect(4).to eq(5) and even THIS passed!
Help much appreciated here!
The 'get' should not have a do block. This will cause the test to always pass.
so this:
it 'should sort the posts by id, in descending order when a descending order is specified' do
get "/api/posts?tags=tech&direction=desc" do. # <<< remove this
expect(JSON.parse(response.body['posts'][0]['id'].value)).to_be(99)
expect(JSON.parse(response.body['posts'][-1]['id'].value)).to_be(1)
end # <<< and this from all tests.
end
should look like this:
it 'should sort the posts by id, in descending order when a descending order is specified' do
get "/api/posts?tags=tech&direction=desc"
expect(JSON.parse(response.body['posts'][0]['id'].value)).to_be(99)
expect(JSON.parse(response.body['posts'][-1]['id'].value)).to_be(1)
end
When I run the following test
RSpec.describe LessonsController, type: :controller do
describe 'GET / index' do
let(:lesson1) {FactoryGirl.create(:lesson)}
let(:lesson2) {FactoryGirl.create(:lesson)}
it 'returns an http success' do
get :index
expect(response).to be_success
end
it 'returns all the lessons' do
get :index
expect(assigns[:lessons]).to eq([])
expect(assigns[:lessons]).to eq([lesson1, lesson2])
end
end
end
The second expect, expect(assigns[:lessons]).to eq([lesson1, lesson2]), fails with expected: [#<Lesson id:...>, #<Lesson id:...>] got: #<ActiveRecord::Relation []>.
But then, when I run the following test and it all passes
RSpec.describe LessonsController, type: :controller do
describe 'GET / index' do
let(:lesson1) {FactoryGirl.create(:lesson)}
let(:lesson2) {FactoryGirl.create(:lesson)}
it 'returns an http success' do
get :index
expect(response).to be_success
end
it 'returns all the lessons' do
get :index
expect(assigns[:lessons]).to eq([lesson1, lesson2])
end
end
end
I am wondering why is it that the second test does not fail? I was expecting the second spec to also fail with the same reason as the first one.
I believe it might be due to the let statement.
With that said, I am running rspec-rails, factory_girl_rails and Rails 4. I don't believe it is due to contamination because this effect still occurs even when I run the test in isolation (focus).
First, I'm guessing your controller has some code like this:
#lessons = Lesson.all
Remember, that returns an ActiveRecord::Relation which may not actually hit the database until the last moment it needs to. Also, once an ActiveRecord::Relation fetches its results, it will not re-fetch them unless you call .reload.
Secondly, remember how let works. Code for a let isn't evaluated until you try to access that a variable. So, you get a situation like this:
describe "Something" do
let(:lesson) { Lesson.create! }
it "makes a lesson" do
# right now there are 0 lessons
lesson
# calling `lesson` caused the lesson to be created,
# now there is 1 lesson
end
end
Third, when you turn an ActiveRecord::Relation into an Array, it executes the real database query (in this case, select * from lessons).
With those things in mind, we can contrast the two test cases.
In the first case, lessons are fetched from the database before the lessons are actually created:
it 'returns all the lessons' do
get :index
# No lessons have been created yet
# `select * from lessons` returns no results
expect(assigns[:lessons]).to eq([])
# `lessons` is cached. It won't query the database again
# calling `lesson1` and `lesson2` creates two lessons, but it's too late
# the result has already been cached as []
expect(assigns[:lessons]).to eq([lesson1, lesson2])
end
In the second case, the lessons are created first, then the database query is executed:
get :index
# calling `lesson1` and `lesson2` creates two lessons
# then the AR::Relation runs a query and finds the two lessons
expect(assigns[:lessons]).to eq([lesson1, lesson2])
To demonstrate this, here is an example that should pass:
get :index
expect(assigns[:lessons]).to eq([])
# this causes the lessons to be created
lessons = [lesson1, lesson2]
# use `.reload` to force a new query:
expect(assigns[:lessons].reload).to eq(lessons)
Also, you could use RSpec's let! to create the lessons before running the example.
Is it possible to examine if get request rendered text?
I know there are hacks like response.body == 'any string' but it does not interest me. I'm just wondering if there is "RSpecâ„¢" way to do it.
Having this rspec:
RSpec.describe MyController, type: :controller do
controller do
def index
render text: 'Hellow'
end
end
describe 'rendering' do
subject { get :index }
it { is_expected.to render_template(text: 'Hellow')}
end
end
I would love to be able to call it { is_expected.to render_template(text: 'Hellow')}. It raises:
Failure/Error: it { is_expected.to render_template(text: 'Hellow') }
ArgumentError:
Unknown key: :text. Valid keys are: :layout, :partial, :locals, :count, :file
or maybe it { is_expected.to render_template('Hellow')}
Failure/Error: it { is_expected.to render_template('Hellow') }
expecting <"Hellow"> but rendering with <[]>
Is there any RSpecâ„¢ way to accomplish it?
Testing expect(response.body).to eq('Hellow') is totally appropriate.
The reason is_expected.to render_template isn't working is you aren't rendering a template. If your controller omitted an explicit render call, Rails would render the index template for you, and you could test render_template(:index). You could also render template: :foo and then test render_template(:foo) if you wanted to render a nonstandard template. But when you render text: 'Hellow', you aren't using templates; you're explicitly setting the response body to the text you specify.
If you do render a template, and you want to test the content rendered by that template, that's when render_views comes into play, as gotva mentioned. Even then, you'd be checking for content in response.body, as you can see in RSpec's own examples. As your templates get complicated, the controller specs aren't the appropriate place for this and you should start writing view specs using assert_select or something similar.
When doing functional tests for controllers in rails how can I provide dynamic application instance variables to my test which live in the request.
I have a #station object that is initialized in a before action in my application controller (available in all other controllers), however the #station object is defined by the domain entry of the user e.g.: blue.mydomain.com. So it could have 3 different flavors, and the controller actions params[:id] are only valid for a certain flavor.
Further if I don't give my #station a flavor for my test environment it will fail utterly:
(Here code from a helper that gets called in a before action in my application_controller.rb)
def init_station
if Rails.env == "test"
#station=Station.new('blue')
else
#station=Station.new(domain_flavor_picker)
end
end
ApplicationController
....
before_action :init_station
.....
end
Thus I can only test for 'blue' or switch the flavor in my before action and then mock for different id!
test:
describe MyController do
before do
#id="10215d8da3f4f278cec747f09985b5528ec9e"
end
it "should get index action" do
p assigns(:station) # is nil
get :artist_biography, id: #id, locale: I18n.available_locales.sample
assert_response :success
assert_not_nil assigns(:meta)
assert_not_nil assigns(:nav)
assert_not_nil assigns(:content)
end
end
As you can see I am also in need of providing a locale variable. I managed to mix up that call with I18n.available_locales.sample
How can I dynamically switch or manipulate my #station instance variable?
My issue was that I needed to provide minitest with an initial host! From #smathy answer I knew that I needed a Mock Request for the Controller!
Turns out that it is quite easy to set it in MiniTest if you know how!
Rails provides an ActionDispatch::TestRequest object which in itself seems to be a Rack::MockRequest object:
DEFAULT_ENV = Rack::MockRequest.env_for('/', 'HTTP_HOST' => 'test.host', 'REMOTE_ADDR' => '0.0.0.0', 'HTTP_USER_AGENT' => 'Rails Testing' )
So all I had to do in my test was:
before do
#request.env['HTTP_HOST'] = %w(blue.mydomain.com red.mydomain.com green.mydomain.com).sample
end
to initialize my #station object with a sample of flavored domains.
assigns :station will only return a value after you do the request, ie. after the get line. Until you've done the request none of your controller code has been run for that test.
You also shouldn't use #vars in rspec, use let instead, and a few other things that I've shown below, many of which I learned from BetterSpecs
The Crux of your Issue
Assuming that domain_flavor_picker is a method in your controller then you should just mock that so you can different tests for the different return values of it. So, this shows the context for one of the return values of domain_flavor_picker, but you'd add other contexts for other values:
describe MyController do
let(:id) { "10215d8da3f4f278cec747f09985b5528ec9e" }
describe "GET /artist_biography" do
context "when domain_flavor is blue" do
before do
allow(controller).to receive(:domain_flavor_picker) { "blue" } # <-- MOCK!
end
context "when valid id" do
before { get :artist_biography, id: id, locale: I18n.available_locales.sample }
subject { response }
it { is_expected.to be_success }
it "should assign :meta" do
expect(assigns :meta).to be_present # better to have an actual matcher here
end
it "should assign :nav" do
expect(assigns :nav).to be_present # ditto
end
it "should assign :content" do
expect(assigns :content).to be_present # ditto
end
end
end
end
end
I use HTTP status code symbols in code in a controller such as:
render json: {
auth_token: user.authentication_token,
user: user
},
status: :created
or
render json: {
errors: ["Missing parameter."]
},
success: false,
status: :unprocessable_entity
In the code of my request spec I also would like to use the symbols:
post user_session_path, email: #user.email, password: #user.password
expect(last_response.status).to eq(201)
...
expect(last_response.status).to eq(422)
However each test where I use the symbols instead of integers fails:
Failure/Error: expect(last_response.status).to eq(:created)
expected: :created
got: 201
(compared using ==)
Here is the latest list of HTTP status code symbols in Rack.
The response object responds to several of the symbol types as messages. So you can simply do:
expect(response).to be_success
expect(response).to be_error
expect(response).to be_missing
expect(response).to be_redirect
For the other types, such as :created, you can create a simple custom matcher for this which wraps assert_response:
RSpec::Matchers.define :have_status do |type, message = nil|
match do |_response|
assert_response type, message
end
end
expect(response).to have_status(:created)
expect(response).to have_status(404)
This should work fine for controller specs which have the proper state setup. It will not work for feature specs. I haven't tried with request specs, so your milage may vary there.
The reason this works is it leverages the fact that RSpec controller specs have similar state setup behind the scenes. So when assert_response accesses #response it is available.
This matcher can probably be improved by simply copying the code used by assert_response into the matcher:
RSpec::Matchers.define :have_status do |type, message = nil|
match do |response|
if Symbol === type
if [:success, :missing, :redirect, :error].include?(type)
response.send("#{type}?")
else
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
response.response_code == code
end
else
response.response_code == type
end
end
failure_message do |response|
message or
"Expected response to be a <#{type}>, but was <#{response.response_code}>"
end
end
UPDATE: 2014-07-02
This is now available out of the box with RSpec Rails 3: https://www.relishapp.com/rspec/rspec-rails/v/3-0/docs/matchers/have-http-status-matcher
this works for me:
expect(response.response_code).to eq(Rack::Utils::SYMBOL_TO_STATUS_CODE[:not_found])
On the one hand, response is built with methods like:
success?
redirect?
unprocessable?
full list do: response.methods.grep(/\?/)
On the other hand, Rspec predicates transforms every foo? method to a be_foo matcher.
Not sure you can have the 201 this way unfortunately, but creating a custom matcher is quite easy.
Note Rails test only rely on a few statuses.
With rspec-rails (as of rspec 3) it's possible to use
expect(response).to have_http_status(:created)
Update 2018-06-11:
As of Rails 6, some of the matchers will be replaced (e. g. success by successful).