rails 4 refactor factorygirl创建不准确的数据

时间:2016-03-24 23:45:43

标签: ruby-on-rails ruby factory-bot rspec-rails

我一直在与一个主要的重构器进行斗争,以减少支付控制器并且可以使用一只手。第一步我试图修理我的工厂。现在所有的工厂都能很好地工作,但是当我尝试建立关联FactoryGirl.create(:job, :purchased_with_coupon)时,它会在优惠券上正确设置关联,而不是付款。这意味着支付的价格始终是1.我只是注意到了这一点,你可以看到其他部分被注释掉了。在我开始处理臃肿的控制器之前,我需要弄清楚我的测试。思考?

工厂

FactoryGirl.define do
  factory :job do
    category
    company
    title { FFaker::Company.position }
    location { "#{FFaker::Address.city}, #{FFaker::AddressUS.state}" }
    language_list { [FFaker::Lorem.word] }
    short_description { FFaker::Lorem.sentence }
    description { FFaker::HTMLIpsum.body }
    application_process { "Please email #{FFaker::Internet.email} about the position." }

    trait :featured do |job|
      job.is_featured true
    end

    trait :reviewed do |job|
      job.reviewed_at { Time.now }
    end

    trait :purchased do |job|
      job.reviewed_at { Time.now }
      job.start_at { Time.now }
      job.end_at { AppConfig.product['settings']['job_active_for_day_num'].day.from_now }
      job.paid_at { Time.now }
      payments { |j| [j.association(:payment)] }
    end

    trait :purchased_with_coupon do |job|
      job.reviewed_at { Time.now }
      job.start_at { Time.now }
      job.end_at { AppConfig.product['settings']['job_active_for_day_num'].day.from_now }
      job.paid_at { Time.now }
      association :coupon, factory: :coupon
      payments { |j| [j.association(:payment)] }
    end

    trait :expired do |job|
      start_at = (200..500).to_a.sample.days.ago
      job.reviewed_at { start_at }
      job.start_at { start_at }
      job.end_at { |j| j.start_at + AppConfig.product['settings']['job_active_for_day_num'].days }
      job.paid_at { start_at }
      payments { |j| [j.association(:payment)] }
    end
  end
end

FactoryGirl.define do
  factory :payment do
    job
    # price_paid { rand(100..150) }
    price_paid { 1 }
    stripe_customer_token { (0...50).map { (65 + rand(26)).chr }.join }
  end
end

FactoryGirl.define do
  factory :coupon do
    code { rand(25**10) }
    percent_discount { rand(100**1) }
    start_at { 2.days.ago }
    end_at { 30.day.from_now }

    trait :executed do |c|
      association :job, factory: [:job, :purchased]
      c.executed_at { Time.now }
    end
  end
end

模型

class Job < ActiveRecord::Base
  acts_as_paranoid
  strip_attributes

  acts_as_taggable
  acts_as_taggable_on :languages

  belongs_to :company
  before_validation :find_company
  belongs_to :category
  has_one :coupon
  has_many :payments

  before_create :create_slug, :set_price
  after_create :update_vanity_url

  accepts_attachments_for :company
  accepts_nested_attributes_for :company
  accepts_nested_attributes_for :coupon
  accepts_nested_attributes_for :payments

  validates :title,
            :location,
            :short_description,
            presence: true,
            format: { with: /\A[\w\d .,:-@]+\z/, message: :bad_format }

  validates :application_process,
            presence: true,
            format: { with: %r{\A[\w\d .,:/@&=?-]+\z}, message: :bad_format }

  validates :title, length: { minimum: 10, maximum: 45 }
  validates :location, length: { minimum: 10, maximum: 95 }
  validates :short_description, length: { minimum: 10, maximum: 245 }
  validates :application_process, length: { minimum: 10, maximum: 95 }

  validates :description,
            :category_id,
            :language_list,
            presence: true

  validates :reviewed_at,
            :start_at,
            :end_at,
            :paid_at,
            date: { allow_blank: true }

  validates :start_at, date: { before: :end_at, message: :start_at_before_end_at }, if: proc { start_at? }
  validates :end_at, date: { after: :start_at, message: :end_at_after_start_at }, if: proc { end_at? }

  scope :active, -> { where.not(reviewed_at: nil, paid_at: nil).where('end_at >= ?', Date.today) }

  def expired?
    end_at.present? && end_at < Date.today
  end

  def reviewed?
    reviewed_at.present?
  end

  def paid_for?
    reviewed? && paid_at.present?
  end

  def active?
    reviewed? && paid_at.present? && end_at <= Date.today
  end

  private

  def set_price
    self.price = AppConfig.product['settings']['job_base_price']
  end

  def create_slug
    self.slug = title.downcase.parameterize
  end

  def update_vanity_url
    self.vanity_url = '/jobs/' + company.slug + '/' + slug + '/' + id.to_s + '/'
    save
  end

  def find_company
    existing_company = Company.where(email: company.email) if company
    self.company = existing_company.first if existing_company.count > 0
  end
end

class Coupon < ActiveRecord::Base
  acts_as_paranoid
  strip_attributes

  belongs_to :job

  validates :start_at, date: { before: :end_at }
  validates :executed_at, date: { allow_blank: true }

  validates_presence_of :job, if: proc { executed_at? }
  validates_presence_of :executed_at, if: :job

  validates :code,
            presence: true,
            length: { minimum: 10, maximum: 19 },
            uniqueness: { case_sensitive: false },
            numericality: { only_integer: true }

  validates :percent_discount,
            inclusion: { in: 1..100 },
            length: { minimum: 1, maximum: 3 },
            numericality: { only_integer: true },
            presence: true

  scope :active, -> { where('start_at < ? AND end_at > ? AND executed_at IS ?', Date.today, Date.today, nil) }

  def active?
    start_at < Date.today && end_at > Date.today && executed_at.nil?
  end

  def executed?
    job_id.present?
  end
end


class Payment < ActiveRecord::Base
  belongs_to :job
  belongs_to :coupon

  validates_presence_of :job
  validate :coupon_must_be_active

  before_create :net_price

  Numeric.include CoreExtensions::Numeric::Percentage

  attr_accessor :coupon_code
  def coupon_code=(code)
    @coupon = Coupon.find_by_code(code)
  end

  def net_price
    return job.price unless @coupon
    job.price = @coupon.percent_discount.percent_of(job.price)
    self.coupon = @coupon
  end

  private

  def coupon_must_be_active
    if @coupon
      errors[:coupon] << I18n.t('flash_messages.coupons.id.inactive') unless @coupon.active?
    elsif @coupon_code.present?
      errors[:coupon_code] << I18n.t('flash_messages.coupons.id.not_found')
    end
  end
end

1 个答案:

答案 0 :(得分:2)

问题似乎是模型之外的逻辑正在更新price_paid上的Payment列,并且可能还会在其上设置coupon_id。< / p>

因此,我建议将可能来自控制器,服务类等的任何额外逻辑复制到工厂的after(:create)回调中。

trait :purchased_with_coupon do
  # ...other attributes...

  association :coupon

  after(:create) do |job, evaulator|
    discount_value = 100 - job.coupon.percent_discount) / 100.0
    calculated_price_paid = job.price * discount_value
    create(:payment, price_paid: price_paid, job: job, coupon: coupon)
  end
end

现在最终,该代码属于某种抽象,例如可以轻松测试(并在其他测试中使用)的服务类。但是,您提到您正在开始使用重构并希望通过测试。我认为这是一个合理的妥协,直到你准备好抽象它。最终,我会做这样的事情:

class CreatePaymentWithCoupon
  attr_reader :job

  def initialize(job)
    @job = job
  end

  def call
    job.payments.create(coupon: job.coupon, price_paid: discounted_price)
  end

  private

  def discounted_price
    discount_value = (100 - job.coupon.percent_discount) / 100.0
    job.price * discount_value
  end
end

然后,在您的规格中:

it "calculates discounted price" do
  coupon = create(:coupon, percent_discount: 25)
  job = create(:job, :purchased_with_coupon, price: 100)
  CreatePaymentWithCoupon.new(job).call

  expect(job.payments.first.price_paid).to eq(75.0)
end