Reputation: 89
I have a customer model that belongs to user, and my controller test for post#create succeeds. But I have a subscription model that belongs to both user and plan, and it is failing (I'm using rails 5.1.2).
Here's my spec:
#rspec/controllers/checkout/subscriptions_controller_spec.rb
require 'rails_helper'
RSpec.describe Checkout::SubscriptionsController, type: :controller do
describe 'POST #create' do
let!(:user) { FactoryGirl.create(:user) }
before do
sign_in user
end
context 'with valid attributes' do
it 'creates a new subscription' do
expect { post :create, params: { subscription: FactoryGirl.attributes_for(:subscription) } }.to change(Subscription, :count).by(1)
end
end
end
end
Subscription controller:
# app/controllers/checkout/subscriptions_controller.rb
module Checkout
class SubscriptionsController < Checkout::CheckoutController
before_action :set_subscription, only: %i[edit update destroy]
before_action :set_options
def create
@subscription = Subscription.new(subscription_params)
@subscription.user_id = current_user.id
if @subscription.valid?
respond_to do |format|
if @subscription.save
# some code, excluded for brevity
end
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: @subscription.errors, status: :unprocessable_entity }
end
end
end
private
def set_subscription
@subscription = Subscription.find(params[:id])
end
def set_options
@categories = Category.where(active: true)
@plans = Plan.where(active: true)
end
def subscription_params
params.require(:subscription).permit(:user_id, :plan_id, :first_name, :last_name, :address, :address_2, :city, :state, :postal_code, :email, :price)
end
end
end
Subscription model -
# app/models/subscription.rb
class Subscription < ApplicationRecord
belongs_to :user
belongs_to :plan
has_many :shipments
validates :first_name, :last_name, :address, :city, :state, :postal_code, :plan_id, presence: true
before_create :set_price
before_update :set_price
before_create :set_dates
before_update :set_dates
def set_dates
# some code, excluded for brevity
end
def set_price
# some code, excluded for brevity
end
end
I'm also using some FactoryGirl factories for my models.
# spec/factories/subscriptions.rb
FactoryGirl.define do
factory :subscription do
first_name Faker::Name.first_name
last_name Faker::Name.last_name
address Faker::Address.street_address
city Faker::Address.city
state Faker::Address.state_abbr
postal_code Faker::Address.zip
plan
user
end
end
# spec/factories/plans.rb
FactoryGirl.define do
factory :plan do
name 'Nine Month Plan'
description 'Nine Month Plan description'
price 225.00
active true
starts_on Date.new(2017, 9, 1)
expires_on Date.new(2018, 5, 15)
monthly_duration 9
prep_days_required 5
category
end
end
# spec/factories/user.rb
FactoryGirl.define do
factory :user do
name Faker::Name.name
email Faker::Internet.email
password 'Abcdef10'
end
end
When I look at the log, I notice that user and plan aren't being populated when running the spec and creating the subscription, which must be why it's failing, since plan is required. But I can't figure out how to fix this. Any ideas? Thanks in advance.
Upvotes: 2
Views: 1397
Reputation: 28285
The issue is that, by your model definition, you can only create a Subscription
that is associated to an existing Plan
:
class Subscription < ApplicationRecord
belongs_to :plan
validates :plan_id, presence: true
end
You could have debugged this issue by either setting a breakpoint in the rspec
test and inspecting the response.body
; or similarly instead by setting a breakpoint in SubscriptionsController#create
and inspecting @subscription.errors
. Either way, you should see the error that plan_id
is not present (so therefore the @subscription
did not save).
The issue stems from the fact that FactoryGirl#attributes_for
does not include associated model IDs. (This issue has actually been raised many times in the project, and discussed at length.)
You could just explicitly pass a plan_id
in the request payload of your test, to make it pass:
it 'creates a new subscription' do
expect do
post(
:create,
params: {
subscription: FactoryGirl.attributes_for(:subscription).merge(post_id: 123)
}
end.to change(Subscription, :count).by(1)
end
However, this solution is somewhat arduous and error prone. A more generic alternative I would suggest is define the following spec helper method:
def build_attributes(*args)
FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].include?(k)
end
end
This utilises the fact that build(:subscription).attributes
does include foreign keys, as it references the associations.
You could then write the test as follows:
it 'creates a new subscription' do
expect do
post(
:create,
params: {
subscription: build_attributes(:subscription)
}
)
end.to change(Subscription, :count).by(1)
end
Note that this test is still slightly unrealistic, since the Post
does not actually exist in the database! For now, this may be fine. But in the future, you may find that the SubscriptionController#create
action actually needs to look up the associated Post
as part of the logic.
In this case, you'd need to explicitly create the Post
in your test:
let!(:post) { create :post }
let(:subscription) { build :subscription, post: post }
...And then send the subscription.attributes
to the controller.
Upvotes: 2