Steve
Steve

Reputation: 89

Rspec controller test failing post #create with association

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

Answers (1)

Tom Lord
Tom Lord

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

Related Questions