I Added customer validation in my controller like
def customer_create
if params[:api_key].present?
## Create Customer
else
## Render Error Message in Json format
end
end
How can I write Rspec Testing for above method?
Thanks In Advance
presumably customer_create is called from your controller's create action?
So it might be sufficient to simply test that action.
describe CustomersController do
it "creates customer if :api_key is present` do
post :create, api_key: "present key", customer_attributes
expect(Customer.count).to eq 1
end
it "does not create customer if :api_key is absent` do
json_error = {
key1: 'value1',
key2: 'value2'
}.to_json
post :create, customer_attributes
expect(response.body).to eq json_error
end
end
You can test the method directly, if you set up the params.
describe CustomersController do
it "creates customer if :api_key is present' do
controller.params[:api_key] = 'present key'
controller.params.merge!(customer_attributes)
controller.customer_create
expect(Customer.count).to eq 1
end
end
Both examples assume customer attributes hash is stored in a variable customer_attributes
Related
I have two problems when I try to test the update action with RSpec, here is the controller file:
#volunteers_controller.rb
module Api
module V1
class VolunteersController < ApplicationController
before_action :find_volunteer, only: %i[show update destroy]
def update
#volunteer.update!(volunteer_params)
head :no_content
end
private
def find_volunteer
#volunteer = Volunteer.find_by!(id: params[:id])
end
def volunteer_params
params.require(:volunteer).permit(:image_url, :name, :job_desc)
end
end
end
end
Here is the test file:
require 'rails_helper'
RSpec.describe Api::V1::VolunteersController, type: :request do
...
describe '#update' do
let(:volunteer) { Volunteer.create!( :image_url=>"first.jpg", :name=>"test1", :job_desc=>"description") }
let(:params){
{:volunteer => {
"image_url"=>"new.jpg",
"name"=>"test1",
"job_desc"=>"description"
}
}
}
it 'updates a certain volunteer' do
patch :patch, :params => params #failed, bad URL
expect(volunteer.image_url).to eq("new.jpg") #failed, still return 'first.jpg'
end
it 'returns a no_content header' do
patch "http://localhost:3000/api/v1/volunteers/#{volunteer.id}", :params => params
expect(response).to have_http_status "204"
end
end
end
private
def json_parse(string)
if string.class==String
json = JSON.parse(string)
end
json
end
So my questions are:
when try to write the URL like this: patch :patch, :params => params, I got the following error:
Api::V1::VolunteersController#update updates a certain volunteer
Failure/Error: patch :patch, :params => params
URI::InvalidURIError:
bad URI(is not URI?): "http://www.example.com:80patch"
How can I change the URL to: "http://localhost:3000/api/v1/volunteers/#{volunteer.id}"?
I manually test the update action, putting a binding.pry in the update action, it does update volunteer subject, however, when it goes back to the test, it shows that it doesn't not get updated, why is that?
Thank you!!
The first problem is really your update method itself and its complete lack of error handling and meaningful feedback to the client. update! will raise ActiveRecord::RecordInvalid if the input is invalid - which is not rescued at all in your controller. And exceptions should no be used for normal code flow - invalid input is not really an exceptional event.
Instead you should rewrite your controller so that it checks if the update is performed and returns the appropriate response:
def update
if #volunteer.update(volunteer_params)
head :no_content
else
head :unprocessable_entity
end
end
As for the spec itself you're mixing up controller specs and request specs. While they look somewhat similar the key difference is that a request spec sends actual HTTP requests your rails server while a controller spec stubs the actual request and passes it to an instance of the controller under test.
In a controller spec you could write:
patch :update, params: { ... }
Because its actually calling the update method on an instance of the controller. But of course:
patch :patch, :params => params #failed, bad URL
Will not work in request spec since its not a valid URL and request specs send actual HTTP requests. Note that you should pass relative URLs and not absolute URLs as the test server may run on a different port then the dev server
# Bad
patch "http://localhost:3000/api/v1/volunteers/#{volunteer.id}", :params => params
# Good
patch "/api/v1/volunteers/#{volunteer.id}", params: params
ActiveRecord models are not "live reloading" - the representation in memory will not automatically be updated when the values in the database are updated. You need to manaully reload the record for that to happen:
it 'updates a certain volunteer' do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
volunteer.reload
expect(volunteer.image_url).to eq("new.jpg")
end
Altogether your spec should actually look something like:
# Describe the endpoint - not the controller implmentation
RSpec.describe "V1 Volunteers API", type: :request do
describe 'PATCH /api/v1/volunteers/:id' do
# use do ... end if the expression does not fit on one line
let(:volunteer) do
# enough with the hashrockets already!
Volunteer.create!(
image_url: "first.jpg",
name: "test1",
job_desc: "description"
)
end
context "with invalid parameters" do
# some set of failing parameters
let(:params) do
{
volunteer: {
name: ""
}
}
end
it "returns unproccessable entity" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect(resonse).to have_http_status :unproccessable_entity
end
it "does not update the volunteer" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect { volunteer.reload }.to_not change(volunteer, :name).to("")
end
end
context "with valid parameters" do
# some set of failing parameters
let(:params) do
{
volunteer: {
image_url: "new.jpg",
name: "test1",
job_desc: "description"
}
}
end
it "returns no content" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect(resonse).to have_http_status :no_content
end
it "updates the volunteer" do
patch "/api/v1/volunteers/#{volunteer.id}", params: params
expect { volunteer.reload }.to change(volunteer, :image_url)
.to("new.jpg")
end
end
end
end
I have created a Chatroom Model that first validates for the presence of some fields so in the controller I create the chatroom then check if it is valid by using the .valid? method to determine the response. Now when I created a test model using FactoryBot the test doesn't go past the if statement and it returns a response as if the test has finished.
Code for my action
def create
new_chatroom = Chatroom.create(chatroom_params)
if new_chatroom.valid?
new_chatroom.members.create({ user_id: #current_user[:username] })
render_response(new_chatroom, :created)
else
render_error_response(new_chatroom.errors, :bad_request)
end
end
Code for the factory
FactoryBot.define do
factory :chatroom do
topic { Faker::Lorem.unique.question }
slug { Faker::IndustrySegments.unique.sub_sector }
description { Faker::Lorem.paragraph }
owner { Faker::Name.first_name }
public { true }
end
end
Here is my test
it "creates a new chatroom" do
post :create, params: {
:topic => "test chatroom",
:slug => "code-testing",
:description => "Testing with Rspec",
}
expect(response).to have_http_status(:created)
end
Here is the render_response method:
def render_response(resource, status)
if block_given?
yield(resource, status)
else
render json: resource, :status => status
end
end
Test failure:
Failure/Error: expect(response).to have_http_status(:created)
expected the response to have status code :created (201) but it was :ok (200)
I get this failure and when I try to make it pass(false positive), the coverage shows the response I'm testing against is not what's actually in my action because the rest of the lines starting from the if statement are not covered.
but I thought FactoryBot takes over the whole model creation in the
tests.
No - FactoryBot just provides factories that create model instances. This is widely used as a replacement for fixtures to populate the database before tests. Unlike with fixtures this is not automatic.
Just adding FactoryBot changes absolutely nothing in your application besides the fact that the generators will create the factory file. It does not effect the behaviour of your models in any way.
When testing the creation of resources you need to test that:
Given valid params, then a model should be persisted to the database
Given valid params, then the response should be successful and point to the newly created resource.
Given invalid params, then a model should not be persisted to the database
Given invalid params, then the response should be 422 and an error page should be rendered.
You want to test this with a request spec and not a controller spec.
Request specs provide a high-level alternative to controller specs. In
fact, as of RSpec 3.5, both the Rails and RSpec teams discourage
directly testing controllers in favor of functional tests like request
specs.
require "rails_helper"
RSpec.describe "Chatroom creation", type: :request do
let(:valid_params) do
{
chatroom: {
topic: "test chatroom",
slug: "code-testing",
description: "Testing with Rspec"
}
}
end
let(:invalid_params) do
{
chatroom: {
topic: ''
}
}
end
context "when the parameters are valid" do
it "creates a new chatroom" do
expect do
post '/chatrooms', params: valid_params
end.to change(Chatroom, :count).by(1)
end
it "returns success" do
post '/chatrooms', params: valid_params
expect(response).to have_http_status(:created)
end
end
context "when the parameters are invalid" do
it "does not create a new chatroom" do
expect do
post '/chatrooms', params: invalid_params
end.to_not change(Chatroom, :count)
end
it "returns bad entity" do
post '/chatrooms', params: invalid_params
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
Then we can address the problem with your controller which should read:
class ChatroomsController < ApplicationController
# ...
def create
new_chatroom = Chatroom.new(chatroom_params)
if new_chatroom.save
new_chatroom.members.create(user_id: #current_user[:username])
render_response(new_chatroom, :created)
else
render_error_response(new_chatroom.errors, :bad_request)
end
end
end
You should never use .valid? to check if a record was saved to the database. It only ensures that the model level validations have passed. Not that the INSERT statement from .create actually created a row in the database. See The Perils of Uniqueness Validations for an example of what can happen.
While you can use new_chatroom.persisted? this is the common rails idiom since it gives you a variable that you can manipulate before the record is persisted.
class ChatroomsController < ApplicationController
# ...
def create
new_chatroom = Chatroom.new(chatroom_params)
new_chatroom.members.new(user: current_user)
if new_chatroom.save
render_response(new_chatroom, :created)
else
render_error_response(new_chatroom.errors, :bad_request)
end
end
end
I would not try to test model validity in a controller spec, but rather write request specs (as suggested by the other respondent). You can test the validity of an object on the model itself like this:
context 'valid' do
let(:chatroom) { create :chatroom }
it { expect(chatroom).to be_valid }
end
context 'fails validation' do
let(:chatroom) { create :chatroom, topic: nil }
it { expect(chatroom).not_to be_valid }
end
Resource: https://www.rubydoc.info/gems/rspec-rails/RSpec%2FRails%2FMatchers:be_valid
But if you want to check that actual fields are validated, I recommend using shoulda matchers on the model like this:
it { should validate_presence_of :topic }
it { should validate_presence_of :slug }
Resource: https://github.com/thoughtbot/shoulda-matchers
I have a problem with calling 'POST' method only once in my test suits.
let(:foo) {post :foo_controller arguments}
it 'FIRST: should validate post response first field' do
expect(foo[:first_field]).to match('something')
end
it 'SECOND: should validate post response second field' do
expect(foo[:second_field]).to match('something else')
end
Now foo's "POST" action is called twice.
I would like to make it that FIRST requests 'POST' and gets a request value, but SECOND only gets a value, which is persisted, without calling this 'POST'.
Is there an elegant way to solve this problem?
You could use a before(:all) block, not sure what post actually returns though.
before(:all) do
#my_response = post :foo_controller arguments
end
Hope that helps!
I us a small helper to fix this issue for me.
This is the helper module:
module ControllerSpecHelpers
# example:
#
# describe 'my request' do
# examine_response {get '/foo/bar'}
# it {should be_ok}
# end
#
#
def examine_response &block
before(:all) do
self.instance_exec &block
end
subject {last_response}
end
end
I configure Rspec to use it in my spec helper:
RSpec.configure do |conf|
# snip ...
conf.extend ControllerSpecHelpers
end
Then when I need to execute the call only once, and test multiple properties, I use it like this:
describe "when signing up" do
examine_response do
post "/signup", {email: 'signup#test.com', password: 'password'}
end
it {should be_ok}
it "body says welcome" do
expect(subject.body).to include 'welcome'
end
end
Here's more detail on how extending Rspec works:
http://timnew.github.io/blog/2012/08/05/extend-rspec-dsl/
I have a users_controller_spec.rb with this:
describe "POST create" do
describe "with valid params" do
let(:user) { create(:user) }
it "assigns a newly created user as #user" do
post :create, user: user
assigns(:user).should be_a(User)
assigns(:user).should be_persisted
end
end
...
end
Debuggin I found that the controller receive the next params
(rdb:1) pp params
{"user"=>"1", "controller"=>"users", "action"=>"create"}
Why "user" => "1" ?, why is not passing the user object properly ?
post :create expects attributes for the user model that it will use to create a user record. you are seeing "user" => "1" because it is passing in the id of the user you created into the :user parameter.
You dont want to create a user record to test the create action. You want to create a hash of attributes for the create action to create the record.
You could write something like this (assuming this would pass your model validations):
user_attributes = { :email => "something#example.com", :username => "something" }
post :create, user: user_attributes
I'm trying to get RSpec working for a simple scaffolded app, starting with the rspec scaffold tests.
Per the devise wiki, I have added the various devise config entries, a factory for a user and an admin, and the first things I do in my spec controller is login_admin.
Weirdest thing, though... all my specs fail UNLESS I add the following statement right after the it ... do line:
dummy=subject.current_user.inspect
(With the line, as shown below, the specs pass. Without that line, all tests fail with the assigns being nil instead of the expected value. I only happened to discover that when I was putting some puts statements to see if the current_user was being set correctly.)
So what it acts like is that dummy statement somehow 'forces' the current_user to be loaded or refreshed or recognized.
Can anyone explain what's going on, and what I should be doing differently so I don't need the dummy statement?
#specs/controllers/brokers_controller_spec.rb
describe BrokersController do
login_admin
def valid_attributes
{:name => "Bill", :email => "rspec_broker#example.com", :company => "Example Inc", :community_id => 1}
end
def valid_session
{}
end
describe "GET index" do
it "assigns all brokers as #brokers" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :index, {}, valid_session
assigns(:brokers).should eq([broker])
end
end
describe "GET show" do
it "assigns the requested broker as #broker" do
dummy=subject.current_user.inspect # ALL SPECS FAIL WITHOUT THIS LINE!
broker = Broker.create! valid_attributes
get :show, {:id => broker.to_param}, valid_session
assigns(:broker).should eq(broker)
end
end
and per the devise wiki here is how I login a :user or :admin
#spec/support/controller_macros.rb
module ControllerMacros
def login_admin
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:admin]
sign_in Factory.create(:admin) # Using factory girl as an example
end
end
def login_user
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in user
end
end
end
What a struggle! Thank you Robin, I've been googling on this for hours and finally saw your post; now my controller tests are working :)
To add to your answer, I figured out how to get the devise session into the valid_session hash, which allows the controller tests to run properly as generated by rails.
def valid_session
{"warden.user.user.key" => session["warden.user.user.key"]}
end
In your tests, there is the following code:
def valid_session
{}
end
...
get :index, {}, valid_session
Because of this 'session' variable, the "log_in" that you did is essentially not being used during the 'get'.
The way that I solved it was to remove all of the "valid_session" arguments to the get, post, put, delete calls in that controller's spec. The example above becomes:
get :index, {}
I suspect that there's a way to add the devise's session to the "valid_session" hash, but I don't know what it is.
Thanks for this solution.
If you are using a different Devise model, the session id also changes.
For a model Administrator use the following:
def valid_session
{'warden.user.administrator.key' => session['warden.user.administrator.key']}
end