As the title suggests I'm just trying to test the create action in my API controller with RSpec. The controller looks something like:
module Api
module V1
class BathroomController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:create]`
def create
bathroom = Bathroom.new(bathroom_params)
bathroom.user = current_user
if bathroom.save
render json: { status: 'SUCCESS', message: 'Saved new bathroom', bathrooms: bathroom }, status: :ok
end
end
private
def bathroom_params
params.require(:bathroom).permit(:establishment, :address, :city, :state, :zip, :gender, :key_needed, :toilet_quantity)
end
end
end
end
Right now this is doing exactly what it should which is great. The test however...not so much. Here's what I have for the test portion:
describe "POST #create" do
let!(:bath) {{
establishment: "Fake Place",
address: "123 Main St",
city: "Cityton",
state: "NY",
zip: "11111",
gender: "Unisex",
key_needed: false,
toilet_quantity: 1
}}
let!(:params) { {bathroom: bath} }
it "receives bathroom data and creates a new bathroom" do
post :create, params: params
bathroom = Bathroom.last
expect(bathroom.establishment).to eq "Fake Place"
end
end
I'm sure there's more than one thing wrong here but I'm having trouble finding much information about the right way to go about testing this. Any insight or suggestions would be greatly appreciated.
I would skip controller specs altogether. Rails 5 has pretty much delegated ActionController::TestCase (which RSpec wraps as controller specs) to the junk drawer. Controller tests don't send real http requests and stub out key parts of Rails like the router and middleware. Total depreciation and delegation to a separate gem will happen pretty soon.
Instead you want to use a request spec.
RSpec.describe "API V1 Bathrooms", type: 'request' do
describe "POST /api/v1/bathrooms" do
context "with valid parameters" do
let(:valid_params) do
{
bathroom: {
establishment: "Fake Place",
address: "123 Main St",
city: "Cityton",
state: "NY",
zip: "11111",
gender: "Unisex",
key_needed: false,
toilet_quantity: 1
}
}
end
it "creates a new bathroom" do
expect { post "/api/v1/bathrooms", params: valid_params }.to change(Bathroom, :count).by(+1)
expect(response).to have_http_status :created
expect(response.headers['Location']).to eq api_v1_bathroom_url(Bathroom.last)
end
it "creates a bathroom with the correct attributes" do
post "/api/v1/bathrooms", params: valid_params
expect(Bathroom.last).to have_attributes valid_params[:bathroom]
end
end
context "with invalid parameters" do
# testing for validation failures is just as important!
# ...
end
end
end
Also sending a bunch of junk like render json: { status: 'SUCCESS', message: 'Saved new bathroom', bathrooms: bathroom }, status: :ok is an anti-pattern.
In response you should just send a 201 CREATED response with a location header which contains a url to the newly created resource or a response body that contains the newly created resource.
def create
bathroom = current_user.bathrooms.new(bathroom_params)
if bathroom.save
head :created, location: api_v1_bathroom_url(bathroom)
else
head :unprocessable_entity
end
end
If your client can't tell by looking at the response code if the response is successful or not you're doing it wrong.
You don't really need to test the values from the record saved on the database, you could do something like:
expect(post :create, params: params).to change(Bathroom, :count).by(1)
That's enough to test that the create action creates a record on the desired table.
Then you can add more specs to test that Bathroom.new receives the expected parameters (that way you know that it would have those fields when saved), or stub the bathroom object and it's save method to test the response.
If you want to test that the saved record has the right values, I think that spec belongs to the Bathroom model and not the controller (or better, an integration test).
So I followed the advice of max but made one slight change to get it working. My final code was:
RSpec.describe "API V1 Bathrooms", type: 'request' do
describe "POST /api/v1/bathrooms" do
context "with valid parameters" do
let(:valid_params) do
{
bathroom: {
establishment: "Fake Place",
address: "123 Main St",
city: "Cityton",
state: "NY",
zip: "11111",
gender: "Unisex",
key_needed: false,
toilet_quantity: 1
}
}
end
it "creates a new bathroom" do
user = FactoryGirl.create(:user, email: "email1#website.com")
login_as(user, :scope => :user)
expect { post "/api/v1/bathrooms", params: valid_params }.to change(Bathroom, :count).by(+1)
expect(response).to have_http_status :created
expect(response.headers['Location']).to eq api_v1_bathroom_url(Bathroom.last)
end
it "creates a bathroom with the correct attributes" do
user = FactoryGirl.create(:user, email: "email2#website.com")
login_as(user, :scope => :user)
post "/api/v1/bathrooms", params: valid_params
expect(Bathroom.last).to have_attributes valid_params[:bathroom]
end
end
end
end
The key was to use FactoryGirl to create a new user because the bathroom needs an associated user_id to be valid.
Related
Here is my rspec test code
require 'rails_helper'
RSpec.describe Users::PaymentController, type: :controller do
let(:user) { create(:user) }
let(:secure_user) { create(:secure_user, user_id: user.id, email: 'abc123#mail.com', birth_date: '1990-01-01', gender: 'female', nation: 'jp', prefecture: 'Tokyo-to', municipality: 'Shibuya-ku') }
describe 'POST #create' do
params = {
birth_date: '2000-01-01',
gender: 'male',
prefecture: 'Osaka-fu',
municipality: 'Osaka-shi'
}
it "changes secure_user's attributes" do
expect do
post :create, params: params
end.to change{ secure_user.birth_date }.from('1990-01-01').to('2000-01-01')
end
end
end
and here is the related part from Users::PaymentController
module Users
class PaymentController < Users::BaseController
def create
current_user.secure_user.update(user_personal_info_params)
......
......
end
private
def user_personal_info_params
params.permit(:birth_date, :gender, :nation, :prefecture, :municipality)
end
end
end
The logic is: add new info to current_user.secure_user while payment is created.
But the test fails and I don't know the reason.
1) Users::PaymentController POST #create changes secure_user's attributes
Failure/Error:
expect do
post :create, params: user_info
binding.pry
end.to change{ secure_user.birth_date }.from('1990-01-01').to('2000-01-01')
expected `secure_user.birth_date` to have changed from "1990-01-01" to "2000-01-01", but did not change
I am not very familiar with RSpec (and so does English), much appreciated if anybody could help me solve this problem.
Try to use .reload
...
end.to change{ secure_user.reload.birth_date }.from('1990-01-01').to('2000-01-01')
The object doesn't get updated inside the test, you have to reload it, to have the up to date attributes.
I am testing my controller with RSPEC using shoulda matchers while i came across the create method in my controller i cant test the save function if i try to do that i go the error
Expected response to be a <3XX: redirect>, but was a <200: OK>
i have attached my controller part and testing and route
In testing
RSpec.describe "routes for home", type: :routing do
describe 'post #create' do
before do
post :create , params: params
end
context 'when the params are correct' do
let(:params) { { restaurant: { restaurantname: "Buhari" ,location_id: 1} } }
it 'is expected save successfully and redirect_to gridpage' do
expect(assigns[:restaurant].save).to redirect_to(gridurl_path)
end
end
end
end
In controller
def create
# render plain: params
#restaurant=Restaurant.new(restaurant_params)
if #restaurant.save
redirect_to gridurl_path
else
render 'index'
end
end
In routes
post "/home/create", to: "home#create", as: :createurl
get '/home/grid', to: 'home#grid',as: :gridurl
Thank you in advance
First I suggest you read https://relishapp.com/rspec/rspec-rails/docs/controller-specs and also the other docs. They will give you a good starting point on how to test stuff with rspec.
When you look at a controller action, you are not interested on who's doing what (i.e assigns[:restaurant]) - you want to see if a redirect happens, if something is saved in the DB, etc. Think of it from the perspective of a user calling that endpoint. Does the user know all of the internals?
Here is how it should look like:
describe "routes for home", type: :controller do
describe 'post #create' do
context 'when the params are correct' do
let(:params) { { restaurant: { restaurantname: "Buhari" ,location_id: 1} } }
it 'is expected save successfully and redirect_to gridpage' do
post :create, params: params
expect(response).to redirect_to('/home/grid')
end
end
end
end
I'm trying to get a create action to set up properly.
I keep getting an error: ArgumentError: Unknown keyword: topic
Here is the testing:
require 'rails_helper'
RSpec.describe TopicsController, type: :controller do
let(:my_topic) { Topic.create!(name: RandomData.random_sentence, description: RandomData.random_paragraph)}
describe "POST create" do
it "increases the number of topics by 1" do
expect{ post :create, {topic: {name: RandomData.random_sentence, description: RandomData.random_paragraph}}}.to change(Topic,:count).by(1)
end
it "assigns Topic.last to #topic" do
post :create, { topic: {name: RandomData.random_sentence, description: RandomData.random_paragraph}}
expect(assigns(:topic)).to eq Topic.last
end
it "redirects to the new topic" do
post :create, {topic: {name: RandomData.random_sentence, description: RandomData.random_paragraph}}
expect(response).to redirect_to Topic.last
end
end
Here is the controller:
def create
#topic = Topic.new
#topic.name = params[:topic][:name]
#topic.description = params[:topic][:description]
#topic.public = params[:topic][:public]
if #topic.save
redirect_to #topic, notice: "Topic was saved successfully."
else
flash.now[:alert] = "Error creating topic. Please try again"
render :new
end
end
I'm trying to figure out what I'm missing that is causing this error I've been staring at it for hours and have tried to edit it multiple times to no avail. I can't figure it out. The rest of the project I've been working on has been okay however I cannot figure out why I can't get the word topic to convert successfully. Thanks for taking a look.
Replace :topic with :params. That's the expected keyword for your test. It is already clear to RSpec that you're testing for Topic since your spec file is TopicsController.
The problem is that the post method takes keyword arguments as a second argument.
If you need to specify params, the params keyword should be used:
post :create, params: { topic: { name: ..., description: ... } }
I have been tearing my hair trying to make the test to pass. I have a JSON API that looks like this:
{
"data": [
{
"id": "b99f8173-0492-457f-9de9-6c1d8d6832ed",
"type": "manufacturer_organizations",
"attributes": {
"account_number": "random test 123"
},
"relationships": {
"organization": {
"data": {
"id": "fb20ddc9-a3ee-47c3-bdd2-f710541ff89c",
"type": "organizations"
}
},
"manufacturer": {
"data": {
"id": "1",
"type": "manufacturers"
}
}
}
},...
I am trying to make a post :create test in rails.
let!(:manufacturer_organization) {FactoryGirl.create(:manufacturer_organization)}
let(:manufacturer_organization2) { FactoryGirl.create(:manufacturer_organization)}
...
describe "POST create" do
it "posts a valid manufacturer organization data" do
authenticate
organization = FactoryGirl.create(:organization)
manufacturer = FactoryGirl.create(:manufacturer)
post :create, manufacturer_organization2.to_json #what should I put here instead?
expect(json['data'].length).to eq(2)
end
#=> error: JSON::ParserError: A JSON text must at least contain two octets!
I have tried various SO posts (this), this, and this article
Here are some of the attempts I have tried:
post :create, params: {organization_id: organization.id, manufacturer: manufacturer.id, account_number: "123 test account number"}
#=> error: JSON::ParserError:
A JSON text must at least contain two octets!
or
post :create, params: :manufacturer_organization_2
#=>
NoMethodError:
undefined method `symbolize_keys' for :manufacturer_organization_2:Symbol
or
json = { :format => 'json', :manufacturer_organization => { :account_number => "foo123", :organization_id => organization.id, :manufacturer_id => manufacturer.id } }
post :create, json
#=> NoMethodError:
undefined method `length' for nil:NilClass
How can I test my controller to accept manufacturer_id, organization_id, and account_number via post :create? Right now the way I test it is to count initial json['data'].length (initially 1); at the end I expect json['data'].length to be 2 after successful post :create. How can I mock creating a valid manufacturer_organization input?
Edit:
Sorry, forgot to put my json method helper:
def json
JSON.parse(response.body)
end
Also, this pass:
describe "POST create" do
it "posts a valid manufacturer organization data" do
authenticate
organization = FactoryGirl.create(:organization)
manufacturer = FactoryGirl.create(:manufacturer)
post :create, {account_number: "Test account numba", organization_id: organization.id, manufacturer_id: manufacturer.id}
expect(response).to be_success
end
while adding expect(json['success']).to eq("Yay!") gives me this error:
JSON::ParserError:
A JSON text must at least contain two octets!
Controller:
def create
#manufacturer_organization = ManufacturerOrganization.new(manufacturer_organization_params)
if #manufacturer_organization.save
puts "success!"
render json: {success: "Yay!"}
else
puts "Sorry, something went wrong!"
end
end
def manufacturer_organization_params
api_params.permit(:organization_id, :manufacturer_id, :account_number)
end
while #api_params ||= ActionController::Parameters.new(ActiveModelSerializers::Deserialization.jsonapi_parse(params))
In RSpec you never* need to explicitly format the params.
post :create, params: { foo: 'bar' }, format: :json
This will properly format the hash { foo: 'bar' } as JSON in the request body.
To create a hash which matches the JSONAPI.org structure you can create a helper:
# support/api_spec_helper.rb
module APISpecHelper
def to_json_api(model)
{
data: {
type: ActiveModel::Naming.plural(model),
attributes: model.attributes
}.tap do |hash|
hash[:id] = model.id if model.persisted?
end
}
end
end
You can also use the JSONAPI-RB gem or ActiveModel::Serializers to constuct/deconstruct JSONAPI responses/params.
require 'rails_helper'
require 'api_spec_helper'
RSpec.request "Manufacturer organizations" do
include APISpecHelper
describe "POST '/manufacturer_organizations'" do
let(:valid_params) do
to_json_api(FactoryGirl.build(:manufacturer_organization))
end
let(:invalid_params) do
to_json_api(ManufacturerOrganization.new(
foo: 'bad_value'
))
end
describe "with valid attributes" do
it "creates a manufacturer organization" do
expect do
post '/manufacturer_organizations', params: valid_params, format: :json
end.to change(ManufacturerOrganization, :count).by(+1)
end
it "has the correct response" do
post '/manufacturer_organizations', params: valid_params, format: :json
expect(response).to have_status :created
expect(response.headers[:location]).to eq(
manufacturer_organization_path(ManufacturerOrganization.last)
)
end
end
describe "with valid attributes" do
it "does not create a manufacturer organization" do
expect do
post '/manufacturer_organizations', params: invalid_params, format: :json
end.to_not change(ManufacturerOrganization, :count)
end
it "has the correct response" do
post '/manufacturer_organizations', params: invalid_params, format: :json
expect(response).to have_status :unproccessable_entity
end
end
end
end
Returning the correct status codes.
Returning the correct response codes is pretty simple:
def create
#resource = Resource.create(some_params)
if #resource.save
# you can respond by pointing at the newly created resource but with no body
head :created, location: #resource
# or
render json: #resource,
status: :created,
location: #resource
else
render json: { errors: #resource.errors.full_messages },
status: :unprocessable_entity
end
end
If a POST request did not include a Client-Generated ID and the
requested resource has been created successfully, the server MUST
return a 201 Created status code.
http://jsonapi.org/format/#crud
Other Responses
A server MAY respond with other HTTP status codes.
A server MAY include error details with error responses.
The commonly accepted practice is to use 422 - Unprocessable Entity for validation errors.
One small concern is that you should use a serializer to give the correct JSON response and also serialize the correct error objects.
I’m having a challenge write a RSpec controller test for a PATCH update, because the routing and edit uses a secure edit_id that my model generates, instead of the standard 1,2,3,4,5 (sequenced id) that Rails auto-generates. Basically, I’m not sure how to get my tests to lookup the request to be edited using this edit_id.
My test currently:
describe "PATCH edit/update" do
before :each do
#testrequest = FactoryGirl.build(:request, name: "James Dong")
end
it "located the requested #testrequest" do
patch :update, id: #testrequest.edit_id, request: FactoryGirl.attributes_for(:request)
assigns(:request).should eq(#testrequest)
end
describe "using valid data" do
it "updates the request" do
patch :update, #testrequest.name = "Larry Johnson"
#testrequest.reload
#testrequest.name.should eq("Larry Johnson")
end
end
FactoryGirl helper (I've tried both explicitly adding edit_id and not [i.e., relying on the model to create the edit_id itself], neither makes a difference) code:
FactoryGirl.define do
factory :request do |f|
f.name { Faker::Name.name }
f.email { Faker::Internet.email }
f.item { "random item" }
f.detail { "random text" }
f.edit_id { "random" }
end
end
Controller:
def update
#request = Request.find_by_edit_id(params[:edit_id])
if #request.update_attributes(request_params)
flash[:success] = "Your request has been updated! We'll respond within one business day."
redirect_to edit_request_path(#request.edit_id)
else
render 'edit'
end
end
Routing:
get 'edit/:edit_id', to: 'requests#edit', as: 'edit_request'
patch 'requests/:edit_id', to: 'requests#update', as: 'request'
Ok someone helped me figure this out, and I feel very silly. The "id" that you pass to the patch method can be any id, so instead of trying to set id: edit_it, I should use edit_it in the first place. I.e., the code that works:
before :each do
#testrequest = FactoryGirl.build(:request, name: "James Dong")
end
it "located the requested #testrequest" do
patch :update, edit_id: #testrequest.edit_id, request: FactoryGirl.attributes_for(:request)
assigns(:request).should eq(#testrequest)
end
describe "using valid data" do
it "updates the request" do
patch :update, edit_id: #testrequest.edit_id, request: FactoryGirl.attributes_for(:request, name: "Larry Johnson")
#testrequest.reload
#testrequest.name.should eq("Larry Johnson")
end
end