I'm trying to write some rspec tests to check API endpoints for an API-only application.
Testing error
Failure/Error: expect( res ).to be_success
expected 200 to respond to `success?`
But if the same call (with full api url) is made from another application it works fine and returns a response.
Example from other application:
res = RestClient.get "site.io/api/v1/projects/1"
p JSON.parse(res)
Blog example I'm trying to follow: (http://matthewlehner.net/rails-api-testing-guidelines/).
# spec/requests/api/v1/messages_spec.rb
describe "Messages API" do
it 'sends a list of messages' do
FactoryGirl.create_list(:message, 10)
get '/api/v1/messages'
json = JSON.parse(response.body)
# test for the 200 status-code
expect(response).to be_success
# check to make sure the right amount of messages are returned
expect(json['messages'].length).to eq(10)
end
end
My Application
/requests/projects_spec.rb
require 'rails_helper'
RSpec.describe Project do
describe "show_project" do
before do
#project1 = create(:project)
end
it "Checks if responds successfully" do
res = get '/api/v1/projects/1'
expect( res ).to be_success
end
end
end
/factories/projects.rb
FactoryGirl.define do
factory :project do
name "Thing"
key "123123"
end
end
routes.rb
namespace :api, :defaults => { :format => 'json'} do
namespace :v1 do
resources :projects, only: [:create, :show]
end
end
end
I don't have much experience with testing, so if anyone can point me in the correct direction I would really really appreciate it.
When using Rspec Request Specs, your call to get '/api/v1/projects/1' doesn't need to captured by your res variable. Spec Request tests automatically set the value of response when get '/api/v1/projects/1' is run. The example you're following is correct, it just looks like your missing some knowledge about how much Rspec is handling for you behind the scenes. This makes your test simpler:
it "Checks if responds successfully" do
get '/api/v1/projects/1'
expect(response).to be_success
end
In Rspec Request tests, response is automatically setup by the call the get without you needing to do anything extra.
Related
In my Rails 5 app I have this:
class InvoicesController < ApplicationController
def index
#invoices = current_account.invoices
respond_to do |format|
format.csv do
invoices_file(:csv)
end
format.xml do
invoices_file(:xml)
end
end
end
private
def invoices_file(type)
headers['Content-Disposition'] = "inline; filename=\"invoices.#{type.to_s}\""
end
end
describe InvoicesController, :type => :controller do
it "renders a csv attachment" do
get :index, :params => {:format => :csv}
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(response).to render_template :index
end
end
My problem is that my Spec always passes (!), even when I put a bunch of crap into my index.csv.erb file. It seems that the view file isn't even evaluated / tested by RSpec.
How is this possible? What am I missing here?
Controller tests/specs are these weird stubbed creations born out of the idea of unit testing controllers in isolation. That idea turned out to be pretty flawed and has really fallen out of vogue lately.
Controller specs don't actually make a real HTTP request to your application that passes through the routes. Rather they just kind of fake it and pass a fake request through.
To make the tests faster they also don't really render the views either. Thats why it does not error out as you have expected. And the response is not really a real rack response object either.
You can make RSpec render the views with render_views.
describe InvoicesController, :type => :controller do
render_views
it "renders a csv attachment" do
get :index, format: :csv
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(response).to render_template :index
end
end
But a better and more future proof option is using a request spec.
The official recommendation of the Rails team and the RSpec core team
is to write request specs instead. Request specs allow you to focus on
a single controller action, but unlike controller tests involve the
router, the middleware stack, and both rack requests and responses.
This adds realism to the test that you are writing, and helps avoid
many of the issues that are common in controller specs.
http://rspec.info/blog/2016/07/rspec-3-5-has-been-released/
# spec/requests/invoices
require 'rails_helper'
require 'csv'
RSpec.describe "Invoices", type: :request do
let(:csv) { response.body.parse_csv }
# Group by the route
describe "GET /invoices" do
it "renders a csv attachment" do
get invoices_path, format: :csv
expect(response.headers["Content-Type"]).to eq("text/csv; charset=utf-8")
expect(response).to have_http_status(200)
expect(csv).to eq ["foo", "bar"] # just an example
end
end
end
The format option should be specified outside of the params, i.e. get :index, params: {}, format: :csv}.
Regarding RSpec evaluating views, no, in controller tests, it doesn't, regardless of the format. However, it's possible to test views with RSpec: https://relishapp.com/rspec/rspec-rails/v/2-0/docs/view-specs/view-spec
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 am trying to test my Grape API, but I am receiving a 400 error in my tests, but when I run the action the test is supposed to test, I get a 201 HTTP response as expected. Not sure what is going on here. Below is the specific RSpec test, but you can view the whole project with the factories and the actual Grape API on GitHub at hackcentral/hackcentral. The test below is testing the POST create action on Alpha::Applications. (app/api/alpha/applications.rb)
describe 'POST #create' do
before :each do
#oauth_application = FactoryGirl.build(:oauth_application)
#token = Doorkeeper::AccessToken.create!(:application_id => #oauth_application.id, :resource_owner_id => user.id)
end
context "with valid attributes" do
it "creates a new application" do
expect{
post "http://api.vcap.me:3000/v1/applications?access_token=#{#token.token}", application: FactoryGirl.attributes_for(:application), :format => :json
} .to change(Application, :count).by(1)
end
it "creates a new application, making sure response is #201" do
post "http://api.vcap.me:3000/v1/applications", application: FactoryGirl.attributes_for(:application), :format => :json, :access_token => #token.token
response.status.should eq(201)
end
end
end
I don't understand why are you testing http://api.vcap.me an not localhost?
You usually test the app on the local enviroment. And this is not the right why to test if the server is working either.
Here is an example of how your test should look like.
https://github.com/dblock/grape-on-rails/blob/master/spec/api/ping_spec.rb from an example project
I have a route that matches:
get 'beat' => 'heartbeat#beat', as: => 'beat'
#=> api_v1_beat GET /api/v1/beat(.:format) api/v1/heartbeat#beat
I then have a controller, exceptionally basic!
module Api
module V1
class HeartBeatController < BaseController
def beat
respond json: {}, status: 200
end
end
end
end
Concept is you would ping this every 15-30 seconds to see if were alive.
Now I need a test for this,
require 'spec_helper'
describe Api::V1::HeartBeatController do
context "200" do
it "should ping the heartbeat and return 200" do
get :beat
expect(response.code).to eql '200'
end
end
end
But it fails:
Failure/Error: get :beat
ActionController::UrlGenerationError:
No route matches {:action=>"beat", :controller=>"api/v1/heart_beat"}
uh ??? Its pretty obvious what it states, but maybe I am missing something so rudimentary and basic?
Try:
get 'beat' => 'heart_beat#beat', as: => 'beat'
Note for others:
In Rails 4, the name of the controller object in a route must be in snake_case for multiple word controllers.
Ex:
get 'action' => 'snake_case_controller_name#action'
Write a routing test also:
describe HeartBeatController do
describe "routing" do
it "routes to #beat" do
get("/beat}").should route_to("api/v1/heart_beat#beat")
end
end
end
I am attempting to create a RSpec controller test for a namespaced controller, but rspec doesn't seem able to detect the nesting and generate the proper path for the post :create action.
This is my spec code:
# for: /app/controllers/admin/crm/report_adjustments_controller.rb
require 'spec_helper'
describe Admin::Crm::ReportAdjustmentsController do
render_views
before(:each) do
signin
end
describe "GET 'index'" do
it "returns http success" do
get :index
response.should be_success
end
end
describe "POST 'create'" do
it "creates with right parameters" do
expect {
post :create, report_adjustment: {distributor_id: #ole_distributor.id, amount: "30.0", date: Date.today }
}.to change(Crm::ReportAdjustment, :count).by(1)
response.should be_success
end
end
end
# routes.rb
namespace :admin do
namespace :crm do
resources :report_adjustments
end
end
For this code, the get :index works just fine, but when post :create is called, the following error is generated: undefined method 'crm_report_adjustment_url'
Why would RSpec be smart enough to figure things out with get :index, but not with post :create? How do I get RSpec to properly load the right route, which is admin_crm_report_adjustments_url?
Thanks in advance.
Try posting to the url instead:
post admin_crm_report_adjustments_url
# or
post "/admin/crm/report_adjustments"