验证可依赖于方法的Rails模型属性时出错

时间:2016-04-07 17:35:59

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

我在Rails中没有很多测试经验,所以我不完全理解测试框架是如何工作的。

我收到错误:

Failure/Error: self.slug ||= name.parameterize
NoMethodError: 
undefined method `parameterize' for nil:NilClass

即使我使用FactroyGirl创建一个包含所有属性的新组织(或使用Organization.new(...).save的旧方式),也会发生此错误。

我理解错误发生的原因,我不明白name如何被评估为nil,因此如何以正常工作的方式编写测试。

我已使用puts "short_name: #{org.short_name}"等语句验证了组织及其属性是否存在于测试范围内。

require 'rails_helper'

class Organization < ActiveRecord::Base

[....]

validates :name, :slug, uniqueness: { case_sensitive: false }
  validates :name, :short_name, :slug, presence: true
  validates :name, length: { maximum: 255 }
  validates :short_name, length: { maximum: 20 }
  validates :slug, length: { maximum: 200 }

before_validation :generate_slug
  def generate_slug
    self.slug ||= name.parameterize
  end

  before_validation :generate_short_name
  def generate_short_name
    self.short_name ||= begin
      if name?
        if name.size > 20
          name.split(/[ -]/).first.first(20)
        else
          name
        end
      end
    end
  end
end

架构:

create_table "organizations", force: :cascade do |t|
    t.integer  "organization_type_id",   limit: 4
    t.string   "name",                   limit: 255
    t.string   "short_name",             limit: 20
    t.string   "slug",                   limit: 200
    t.text     "description",            limit: 65535
    t.integer  "year_founded",           limit: 2, unsigned: true
    t.datetime "last_published_date"
    t.date     "notification_sent_date"
    t.datetime "last_imported_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

组织工厂:

FactoryGirl.define do
  factory :organization, class: Organization do
    name 'Example & Co'

    trait :all_fields do
      slug 'example-co'
      short_name 'Example & Co'
      description ‘This is a description.’
      year_founded 2010
    end
  end
end

以下所有验证测试都适用于其他组织属性。 如果用户没有提供slug,则generate_slug方法应该从name属性创建url安全slug。 注意:我不是代码的作者,我只是为承包商构建的应用程序构建测试套件。

试验:

第一次测试正在通过,我将其包含在信息/验证中

RSpec.describe Organization, :type => :model do

  #validating my factory:      
  describe 'FactoryGirl' do
    it 'factory generating all fields should be valid' do
      create(:organization, :all_fields).should be_valid
      build(:organization, :all_fields).should_not be_valid
    end

    it 'factory generating name field should be valid' do
      create(:organization).should be_valid
      build(:organization).should_not be_valid
    end
  end

错误的测试:

describe 'name' do
  let(:org) {FactoryGirl.build(:organization, :all_fields)} 
  context 'is valid' do
    # this is the only test on #name that fails
    it { should validate_presence_of(:name) }
  end
end

describe 'slug' do
  let(:org) {FactoryGirl.build(:organization, :all_fields)}
  context 'is valid' do
    # this is the only test on #slug that fails,     
    it { should validate_presence_of(:slug) }
  end
end

describe 'short_name' do
  # All of the tests on short_name fail
  let(:org) {FactoryGirl.build(:organization, :all_fields)}
  context 'is valid' do
    it { should validate_presence_of(:short_name) }
    it { should have_valid(:short_name).when(org.short_name, 'Example & Co') } 
    it { should validate_length_of(:short_name).is_at_most(20) }      
  end

  context 'is not valid' do
    it { should_not have_valid(:short_name).when('a' * 21) }       
  end
end

1 个答案:

答案 0 :(得分:1)

首先:

您需要在测试中设置正确的主题。这是所有it单行将引用的对象。您确实创建了一个let,但由于您从未明确将其设置为主题,因此测试选择了隐式主题(在您的情况下默认为Organization.new)

要设置明确的主题,您可以写:

describe 'name' do
  subject { FactoryGirl.build(:organization, :all_fields) } 

  it { should validate_presence_of(:name) }
  # or with the new syntax
  it { is_expected.to validate_presence_of(:name) }
end

您可以在此处详细了解隐式与显式主题和单行:http://www.relishapp.com/rspec/rspec-core/v/3-4/docs/subject/one-liner-syntax

第二

另一个问题是,假设您正在使用shoulda-matchers gem,它会将name设置为nil,以便在该属性不存在时查看验证应该发生错误。

但是当名称设置为nil时,before_validation回调会抛出错误,因为它总是假设找到name

你可以修改这样的回调(example in rails documentation):

before_validation :generate_slug
def generate_slug
  self.slug ||= name.parameterize if attribute_present?("name")
end

第三

一个建议。如果您已经设置了工厂并且拥有了shoulda-matchers gem,那么您可以编写非常简洁的规范。例如像这样。

RSpec.describe Organization, :type => :model do
  # If you fix the callback you don't even need 
  # to set explicit subject here

  it { is_expected.to validate_presence_of(:name) }
  it { is_expected.to validate_lenght_of(:name).is_at_most(255) }
  ... etc

  # Add custom contexts only for the before_validation callbacks,
  # because shoulda-matchers cannot test them.
  # One possible way (there are different ways and opinions on how
  # should implement this kind of test):
  describe '#slug' do
    let(:organization) { described_class.new(name: 'Ab cd', slug: slug) }

    before { organization.valid? }

    subject { organization.slug }

    context 'when it is missing' do
      let(:slug) { nil }
      let(:result) { 'ab_cd' }

      it 'gets created' do
        expect(subject).to eq(result)
      end
    end

    context 'when it is not missing' do
      let(:slug) { 'whatever' }

      it "won't change" do
        expect(subject).to eq(slug)
      end
    end
  end
end

有关更多示例,您可以浏览the shoulda-matchers documentation