我有一个属于用户的客户模型,我对post#created的控制器测试成功。但我有一个属于用户和计划的订阅模型,它失败了(我使用的是rails 5.1.2)。
这是我的规范:
#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
订阅控制器:
# 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
订阅模式 -
# 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
我还为我的模型使用了一些FactoryGirl工厂。
# 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
当我查看日志时,我注意到在运行规范和创建订阅时没有填充用户和计划,这必然是它失败的原因,因为需要计划。但我无法弄清楚如何解决这个问题。有任何想法吗?提前谢谢。
答案 0 :(得分:1)
问题是,根据您的模型定义,您只能创建与现有Subscription
相关联的Plan
:
class Subscription < ApplicationRecord
belongs_to :plan
validates :plan_id, presence: true
end
您可以通过在rspec
测试中设置断点并检查response.body
来调试此问题;或类似地通过在SubscriptionsController#create
中设置断点并检查@subscription.errors
。无论哪种方式,您都应该看到plan_id
不存在的错误(因此@subscription
没有保存)。
问题源于FactoryGirl#attributes_for
does not include associated model IDs。 (此问题实际上在项目中been raised many times,并进行了详细讨论。)
你可以只是在测试的请求有效负载中显式传递plan_id
,以使其通过:
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
但是,这种解决方案有些艰巨且容易出错。我建议的一个更通用的替代方法是定义以下规范帮助方法:
def build_attributes(*args)
FactoryGirl.build(*args).attributes.delete_if do |k, v|
["id", "created_at", "updated_at"].include?(k)
end
end
这利用了build(:subscription).attributes
包含外键的事实,因为它引用了关联。
然后您可以按如下方式编写测试:
it 'creates a new subscription' do
expect do
post(
:create,
params: {
subscription: build_attributes(:subscription)
}
)
end.to change(Subscription, :count).by(1)
end
请注意,此测试仍然有点不切实际,因为Post
实际上并不存在于数据库中!现在,这可能没问题。但是在将来,您可能会发现SubscriptionController#create
操作实际上需要查找关联的Post
作为逻辑的一部分。
在这种情况下,您需要在测试中明确创建Post
:
let!(:post) { create :post }
let(:subscription) { build :subscription, post: post }
...然后将subscription.attributes
发送给控制器。