I have a request spec and I wanted to test CRUD.
The problem is whatever request I put first succeeds, but the rest that follows fail. (Meaning, in the case below, GET will succeed, but POST will fail. If
I switch the 2 context blocks, POST then succeeds, then GET fails).
The failing test says the response is 401 Unauthorized. I'm not sure if the api_key suddenly becomes invalid or something. Or it has something to do with the role I assigned it. (I assigned a system_admin role to the user to be able to CRUD via cancancan)
The way that makes it all work is if I put all requests in one big it block (which I think is not good practice since it returns only 1 passing example, when in reality I have more than 1.)
I've also tried signing in my user again in each block but still the same thing happens.
I am very new to RSpec so any help would be appreciated. Thank you very much!
require 'rails_helper'
RSpec.describe "Departments", type: :request do
user = FactoryBot.build(:user)
role = FactoryBot.build(:role, name: "system_admin")
user.add_role(role.name)
headers = { "x-app-api-key" => user.api_key }
before do
sign_in user
end
describe "CRUD /v1/departments" do
context "GET" do
it "called get" do
get api_departments_path, headers: headers
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response).to have_http_status(:success)
end
end
context "POST" do
it "called post" do
post api_departments_path, headers: headers, params: { name: "department" }
expect(response.content_type).to eq("application/json; charset=utf-8")
expect(response).to have_http_status(:success)
end
end
end
end
There are a LOT of problems with this spec. First off the bat is your use of local variables instead of let/let!:
require 'rails_helper'
RSpec.describe "Departments", type: :request do
user = FactoryBot.build(:user)
role = FactoryBot.build(:role, name: "system_admin")
user.add_role(role.name)
The problem with this is that these variables will be set when RSpec sets up the spec and not for each example. So provided you actually did insert the records into the database they will still be wiped after the first example is run and all the subsequent examples will fail.
So lets fix that problem:
require 'rails_helper'
RSpec.describe "Departments", type: :request do
let(:user) { FactoryBot.create(:user) }
let(:role) { FactoryBot.create(:role, name: "system_admin") }
before do
user.add_role(role.name)
end
let is evaluated once per example and then memoized.
Note the use of create instead of build which saves the record in the database so that it can actually be accessed from the controller.
Then there is the problem that these tests don't actually provide any value or describe the behavior of the application that you're testing besides testing the http response code.
Test your application - not your tests. If the failure message doesn't tell you anything about what part of the application doesn't work the test is near worthless.
require 'rails_helper'
RSpec.describe "Departments", type: :request do
let(:user) { FactoryBot.create(:user) }
let(:role) { FactoryBot.create(:role, name: "system_admin") }
let(:headers) do
{ "x-app-api-key" => user.api_key }
end
let(:json) { response.parsed_body }
before do
user.add_role(role.name)
sign_in user
end
describe "GET /v1/departments" do
let(:departments) { FactoryBot.create_list(:department, 5) }
it "responds with a list of the departments" do
get api_departments_path, headers: headers
expect(response).to be_successful
# #todo ensure the JSON response contains the expected departments
end
end
describe "POST /v1/departments" do
context "with invalid parameters" do
let(:params) { { name: "" } } # this should just be an invalid or incomplete set of attributes
it "doesn't create a department" do
expect do
post api_departments_path, headers: headers, params: params
end.to_not change(Department, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
context "with valid parameters" do
let(:params) { FactoryBot.attributes_for(:department) }
it "creates a department" do
expect do
post api_departments_path, headers: headers, params: params
end.to change(Department, :count).by(1)
expect(response).to have_http_status(:created)
# #todo test that either a JSON representation is returned
# or that the response contains a location header with the URI
# to the resource
end
end
end
end
Testing the content type is largely superflous as you set the content type in the defaults for an API app and testing the JSON responses would fail if its not returning application/json.
Related
I have a basic Rails API built with Accounts and Users. All of the account specs pass when I remove...
before_action :authenticate_user!
But with that in place, I'm having trouble getting the specs to pass.
# Note `json` is a custom helper to parse JSON responses
RSpec.describe 'Account API', type: :request do
# test data
let!(:user) { create(:user) }
let!(:accounts) { create_list(:account, 10, user_id: user.id) }
let(:account_id) { accounts.first.id }
# GET /accounts
describe 'GET /accounts' do
# HTTP request before examples
before do
get '/accounts'
request.headers.merge! user.create_new_auth_token
end
it 'returns accounts' do
expect(json).not_to be_empty
expect(json.size).to eq(10)
end
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
end
As you can see I attempted to merge the auth token with this line in a before do block...
request.headers.merge! user.create_new_auth_token
But that is not working. Instead I get json.size == 1 and http_status of 401 unauthorized.
I was able to fix it by declaring a variable at the top...
let(:auth_headers) { create(:user).create_new_auth_token }
And in my specs using it like this...
before { get '/accounts', headers: auth_headers }
Here I have this problem while writing test for this route
close_offer POST (/:locale)/offers/:code/close(.:format) offers#close {:locale=>/en|vi/}
This is the test that I wrote:
RSpec.describe OffersController, type: :controller do
let(:access_token) { extract_access_token_from_vcr_cassette }
describe 'POST #close' do
describe 'successful response' do
before(:each) do
VCR.use_cassette('offers/successful_close_offer') do
post :close, params: { code: 5742, access_token: access_token }, xhr: true
end
end
it 'returns success? = true' do
expect(assigns(:result)).not_to be_nil
end
end
end
end
Somehow, the assigns(:result) always return nil. Although it works perfectly in tests in other routes. The real code also works, only the test failed. It seems that no operation has run, no vcr record, so I suspect that this line post :close, params: { code: 5742, access_token: access_token }, xhr: true is wrong. Anyone has a hint?
Yes, I used to face the same situation. Let's check you authentication. Somehow, you couldn't sign in so that the route is not like you expected, no result returned.
I am new to RSpec but here I am trying to create tests based on this code and I am keep on getting this error. Any suggestions?
CODE:
serialization_scope nil
before_action :set_list, only: [:show, :destroy, :update]
before_action :verify_user, only: :show
def create
#list = current_user.lists.build(list_params)
if #list.save
render json: {message: ['Success']}, status: 200
else
render json: {errors:[#list.errors.full_messages]}, status: 400
end
end
Here is the RSpec file that I started :
require "rails_helper"
RSpec.describe V1::ListsController, :type => :controller do
describe "POST create" do
it "returns HTTP status" do
expect(post :create).to change(#list, :count).by(+1)
expect(response).to have_http_status :success #200
end
end
describe 'GET status if its not created' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
expect(response.status).to eq 400
end
end
end
And the error that I got is :
Failures:
1) V1::ListsController GET status if its created returns HTTP status
Failure/Error: expect(post :create).to change(#list, :count).by(+1)
expected #count to have changed by 1, but was not given a block
# ./spec/controllers/lists_controller_spec.rb:8:in `block (3 levels) in <top (required)>'
2) GET status if its not created return HTTP status - reports BAD REQUEST (HTTP status 400)
Failure/Error: expect(response.status).to eq 400
expected: 400
got: 200
(compared using ==)
Try this code.
require 'rails_helper'
RSpec.describe V1::ListsController, type: :request do
describe 'valid request' do
it 'returns HTTP status' do
post '/list', params: { list: { list_name: 'xyz' } }
expect(response.status).to eq 201
end
end
describe 'invalid request' do
it "should return unauthorized" do
post '/list'
assert_response :unauthorized
end
end
end
In params you need to pass your list_params.
Spec would look like:
describe "POST create" do
context 'valid request' do
it 'should increase #list item' do
expect { post :create }.to change(List, :count).by(1)
end
it "returns HTTP status" do
post :create
expect(response).to have_http_status :success #200
end
end
context 'invalid request' do
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
get :create
expect(response.status).to eq 400
end
end
end
Cheers!
You can test an object not being created by intentionally causing some of its validations to fail e.g. you can pass a mandatory attribute as nil from the RSpec.
Sample request: post :create, { title: nil }.
But as per your RSpec code, it seems there are no validations on List model. So, lets try to stub save and return false for this particular test.
describe 'GET status if its not created' do
# Assuming your model name is `List`
before { allow_any_instance_of(List).to receive(:save) { false } }
it "return HTTP status - reports BAD REQUEST (HTTP status 400)" do
post :create
expect(response.status).to eq 400
end
end
Please post your model for list and i can update the answer with more appropriate test.
Ishika, let me see if I can help you :)
RSpec official documentation recommends you to use request specs instead of controller specs. That is recommended because Rails 5 deprecated some methods used on controller testings. You can read more about this here at RSpec blog
ps.: You can use controller tests so far, but it can be deprecated in a future major version of RSpec.
There are some notes I left after the code, please read them also.
I would write a request spec like this:
# spec/requests/v1/lists_controller_create_spec.rb
require "rails_helper"
RSpec.describe V1::ListsController do
describe 'success' do
it 'returns ok and creates a list', :aggregate_failures do # :aggregate_failures is available only for RSpec 3.3+
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.to change { List.count }.from(0).to(1)
expect(response).to have_http_status(:ok)
end
end
describe 'bad request' do
before do
# This is needed because your controller is not validating the object, but look at my
# comment below (out of the code), to think about this behavior, please.
allow_any_instance_of(List).to receive(:save).and_return(false)
end
it 'returns a bad request and does not create a list' do
expect do
post '/list', title: 'foo' # This will also test your route, avoiding routing specs to be necessary
end.not_to change { List.count }
expect(response).to have_http_status(:bad_request)
end
end
end
Notes:
I suggested using more than 1 expectation by example, that is ok in this spec because they are simple and because I'm using :aggregate_failures option. With this option, if the first expectation fails, the next expectations will also be executed, considering that in this case, the following expectations does not depend on the first one, it is ok to use more than 1 expectation for the example.Reference
You are returning a bad request if the object is not saved, but you are not validating it. If your model has validations that will validate the object there, please adjust the specs to fail the save (instead of using the mock I used) and consider rendering an error message in the response
If you think that making the post inside a expect block, you can do different: Store the count of Lists in a variable before making the post and after the post you test if the variable has changed or not, maybe you think it will be more clear and it will do exactly the same thing in the background.
I have a spec type: :request and I want to add authentication via ApiAuth. How can i do that?
ApiAuth use request object for authentication
ApiAuth.sign!(request, user.id, user.api_secret_key)
And I can use it in specs for controllers as follows
request.env['HTTP_ACCEPT'] = 'application/json'
user = defined?(current_user) ? current_user : Factory.create(:admin_user)
ApiAuth.sign!(request, user.id, user.api_secret_key)
But request object is missing in specs for requests
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
context 'given an unauthorized request' do
it 'returns 401 status' do
get '/api/cities'
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
it 'sends a list of cities' do
# i need authorization here
get '/api/cities'
expect(response).to be_success
end
end
end
Now I stub authentication
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
before { Factory.create(:route) }
context 'given an unauthorized request' do
it 'returns 401 status' do
get '/api/cities'
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
before(:each) do
allow_any_instance_of(CitiesController).to receive(:authenticate_user!).and_return(true)
allow_any_instance_of(CitiesController).to receive(:admin_user).and_return(admin_user)
end
it 'sends a list of cities' do
get '/api/cities'
expect(response).to be_success
end
end
end
But I want to avoid of using stubs in request specs.
Does anybody have any ideas?
I created issue on github. You can watch my solution here
I am trying to test my Rails application using RSpec, but my tests are failing because RSpec seems to not be passing the headers I give it to Rails.
I have a UsersController that includes ApplicationHelper, and in ApplicationHelper I have a method that accesses the headers hash. Indexing it by my SESSION_KEY header returns nil. If I puts headers inside that method, the hash does not contain the header I have supplied, only the following: {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff"}.
Here is the relevant part of my RSpec spec:
require 'rails_helper'
describe Api::V1::UsersController, type: :request do
let(:user) { User.create(name: 'TestUser', email: 'someone#example.com', password: 'password123', password_confirmation: 'password123') }
let(:id) { user.id }
let(:sess) { user.sessions.create }
before { get "/api/v1/users/#{id}" }
# Snipped other tests
context 'with authentication' do
context 'with a valid id' do
it 'returns full user information' do
get "/api/v1/users/#{id}", nil, {'HTTP_SESSION_KEY': sess.key}
response_user = response_json[:user]
expect(response.status).to eq 200
expect(response_user).to_not be_nil
expect(response_user[:name]).to eq user[:name]
expect(response_user[:email]).to eq user[:email]
end
end
end
def response_json
JSON.parse(response.body, symbolize_names: true)
end
end
I have also tried passing the SESSION_KEY header without HTTP_ before it, and that did not work. I have also tried moving it up to the top get in the before block to see if it was a context issue, and that did not work either.
Docs say the above should work, but if for some reason rspec is interpreting your test as a :controller test and not a :request test then you need to do this (just before your get call):
request.env["HTTP_SESSION_KEY"] = sess.key