How to write Rspec for APIs - ruby-on-rails

I have the following code in my API file
class TransactionStatus < Grape::API
helpers ::PushAPIv1::NamedParams
post '/transaction/status' do
Rails.logger.warn "#{params.to_xml}"
// some piece of code
end
end
I tried to write Rpsec for this condition but not getting any success in my coverage report. The Spec I tried to write is as below
require 'rails_helper'
RSpec.describe Transaction, type: :request do
context 'check Transaction Status' do
it 'should log an info message' do
expect(Rails.logger).to receive(:warn).with("#{params.to_xml}")
end
it 'should raise Transaction Not Found Error if invalid transaction' do
transaction = FactoryGirl.build(:transaction, state: 'processing', gateway_message: 'success', ref_code: '1qqqq1')
p transaction.ref_code
expect { Transaction.find_by_ref_code('q1111q').should eq transaction }.to raise_error()
end
end
end

Well, if what you're trying to achieve is coverage of your POST /transaction/status endpoint, then... you need to reach for the endpoint in your specs, which you're not doing at the moment.
it 'should log an info message' do
expect(Rails.logger).to receive(:warn).with("#{params.to_xml}")
end
Here you expect Rails.logger to receive warn message. But you need to trigger something that should call Rails.logger.warn for the spec to pass.
it 'should raise Transaction Not Found Error if invalid transaction' do
transaction = FactoryGirl.build(:transaction, state: 'processing', gateway_message: 'success', ref_code: '1qqqq1')
expect { Transaction.find_by_ref_code('q1111q').should eq transaction }.to raise_error()
end
About this spec: you're mixing expect and should syntaxes in a manner that's hardly understandable. Plus you're just using ActiveRecord methods, and never call your actual API endpoint. That's why you don't get any code coverage.
In the end, what you should do to get proper coverage of your endpoint is actually calling it. This could be done in a before :each block for instance, or even in each of your spec, like this:
describe 'transaction/status' do
before :each do
post 'path/to/api/transaction/status'
# post 'path/to/api/transaction/status', params: { some: params }
# post 'path/to/api/transaction/status', headers: { some: headers }
end
it '...' do
expect( response ).to ...
end
end
You get the idea. You can check out RSpec Rails 3.7 - Request specs for more details and examples.

Related

Request spec only first context is success

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.

Using rspec to test an API endpoint

I have this api endpoint wot get all the blogs from my database that works id the user pass an api_key. This works correctly and now I'm trying to testing this endpoint.
Routes:
Rails.application.routes.draw do
get 'blogs', to: 'blogs#index'
end
Blogs controller:
class BlogsController < ApplicationController
def index
if params[:api_key]
user = User.find_by(api_key: params[:api_key])
if user.present?
#blogs = Blog.all
return render json: #blogs, status: :ok
end
end
render json: { error: "Unauthorized!" }, status: :bad_request
end
end
I'm new to rspec and tests in general, I watched a couple videos and tutorials and this is what I have so far:
spec/requests/blogs_spec.rb
require 'rails_helper'
RSpec.describe 'Blogs API', type: :request do
let!(:blogs) { Blog.limit(10) }
describe 'GET /blogs' do
before { get '/blogs' }
it 'returns status code 400' do
expect(response).to have_http_status(400)
end
context 'when the request is valid' do
before { get '/blogs', params: { api_key: '123123'} }
it 'returns status code 400' do
expect(response).to have_http_status(200)
end
end
end
end
I can't seem to make the last test work and I don't know why. My guess is that I'm not passing api_key correctly, but I don't know how
1) Blogs API GET /blogs when the request is valid returns status code 400
Failure/Error: expect(response).to have_http_status(200)
expected the response to have status code 200 but it was 400
# ./spec/requests/blogs_spec.rb:28:in `block (4 levels) in <top (required)>'
Ok, so accordingly to your question + comments, I can assume you are running your tests within test environment, but you are expecting to find a User existing in development database.
FactoryBot
You might wanna use FactoryBot to create records for your testing suite.
Add to your Gemfile:
group :development, :test do
gem 'factory_bot_rails'
end
In rails_helper.rb, add:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
Now you should create your User factory. Create a new file spec/factories/user.rb with the following:
FactoryBot.define do
factory :user do
api_key { '123123' }
# You should define every any other required attributes here so record can be created
end
end
Finally, in your spec file:
....
context 'when the request is valid' do
before { get '/blogs', params: { api_key: user.api_key} }
let!(:user) { create(:user) }
it 'returns status code 200' do
expect(response).to have_http_status(200)
end
end
...
Now your test should pass. Notice that in testing database there is no Blog created also, so:
let!(:blogs) { Blog.limit(10) }
Will return an empty array. You will need to create a Blog factory too, and create blogs like:
let!(:blogs) { create_list(:blog, 2) }
Bonus
As soon as you start improving your tests, you may wanna take a look at Faker and Database Cleaner for ActiveRecord

Rspec for conditional code if-else?

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.

How to test that a request does not return 500 error?

I am trying to create an RSpec test which detects if a request can crash the controller, usually a 500 error. So I want to be able to distinguish between:
nil.invalid_method # raises NoMethodError
from
params.require(:required_parameter) # raises ActionController::ParameterMissing
in a controller in a generic way. When I do a request,feature or controller test it raises an exception:
describe "Post", type: :request do
it 'does not crash when no params given' do
post '/posts' # this line launches an exception
expect(page).to_not have_http_status(500)
end
end
It seems that before RSpec (or Rails I don't know) had a different behaviour, similar to I'm looking for:
rails 4 api rspec test http status code 410
Rspec shows different status code than browser
How to use HTTP status code symbols in RSpec?
How can I do this? Or how would you do?
Thanks for your time.
You can use a controller spec that doesn't render a 500, but raises the exception instead:
describe "PostController", type: :controller do
describe "POST index" do
it 'does not crash with valid params' do
expect {
post :index, { post: { title: 'foo' } }
}.to_not raise_exception
end
end
describe "POST index" do
it 'crashes without params' do
expect {
post :index
}.to raise_exception(ActionController::ParameterMissing)
end
end
end
Also note the curly brackets { ... } after expect.
You can test that the controller does not raise an uncaught exception by using the raise_error matcher:
RSpec.describe "Things", type: :request do
describe "POST /things" do
it "does not raise an error" do
# we pass a block to expect
expect { post things_path }.to_not raise_error
end
end
end
If the exception is rescued in the controller by using the rescue keyword or Rails rescue_from you would test the response code as usual:
class ThingsController < ApplicationController
rescue_from ActionController::ParameterMissing do
head 500
end
def create
raise ActionController::ParameterMissing.new('foo')
end
end
RSpec.describe "Things", type: :request do
describe "POST /things" do
it "work even if the param is not provided" do
post things_path
expect(response).to successful
end
end
end
In this case it is much more useful to test that the response is what you expected it to be - not that it is not a 500.

Avoid excessive rspec nesting with subject, let, and alternative arguments

I'm trying to do some model_spec testing but having trouble with not having to further nest my rspec code. It would be great if in this case, I could just have a set of "it's" instead of having to add context everytime I want to switch the variable var. Here's the following code:
describe "#some_method" do
subject { course.some_method(var) }
context 'given a project' do
let(:var) {random[1]}
it 'returns the one after' do
is_expected.to eq(random[2])
end
context 'being the last' do
let(:vars) {random.last}
it 'returns nil' do
is_expected.to be_nil
end
end
context '...you get the point, being something else' do
let(:vars) { something.else }
it 'returns nil' do
is_expected.to.to be_nil
end
end
end
end
Maybe I'm just stuck in the wrong mode of thinking and someone could think of a better way for me to do this? I've been suggested that I absolutely must use the subject by someone I work for.
At first, I disagreed and thought it was getting a little burdensome but then I figured keeping subject and having let(:var) apply to it was pretty useful...
RSpecs subject is a tool which can be used to make tests more succinct. There are many cases where it makes sense to use the subject:
RSpec.describe User do
# with the help of shoulda-matchers
it { should validate_uniqueness_of :username } # implicit subject
end
RSpec.describe UsersController do
describe '#show' do
it 'is successful' do
get :show
expect(response).to have_http_status :success
end
it 'renders template show' do
get :show
expect(response).to render_template :show
end
end
#vs
describe '#show' do
subject { response }
before { get :show }
it { should have_http_status :success }
it { should render_template :success }
end
end
And there are cases where using subject will hurt the readability and acuity of your tests.
Your college is just plain wrong in insisting that you always use subject.
A good rule of hand is that if you need an it block then you should not be using subject or is_expected.
If you are describing the call signature of a method you should be calling it in your specs in the same way you would in real life.
let(:decorator){ described_class.new(user) }
describe "#link" do
it 'takes a class option' do
expect(decorator.link(class: 'button')).to match /class=\"button/
end
end
I would recommend running rspec with the --format documentation option and checking if the output actually makes sense. This can be quite important once you get 100s of specs as it gets harder to remember what a behavior a spec actually covers.
How about you write it like this?
expect(subject.call(foo)) is not very pretty but it gets rid of the nesting.
describe "#some_method" do
subject { course.method(:some_method) }
it 'returns the one after if given a project' do
expect(subject.call(random[1])).to eq(random[2])
end
it 'returns nil when it is the last' do
expect(subject.call(random.last)).to be_nil
end
it 'returns nil...' do
expect(subject.call(something.else)).to be_nil
end
end

Resources