我可以使用 FactoryGirl 创建无效值吗?

Can I create invalid values with FactoryGirl?

我有电子邮件工厂 (spec/factories/email.rb):

FactoryGirl.define do
  factory :email, class: String do
    skip_create

    transient do
      username 'user'
      subdomain 'mail'
      domain_name 'example.com'

      host { [subdomain, domain_name].compact.join('.') }
    end

    trait(:with_blank_host) { host '' }
    trait(:with_blank_username) { username '' }

    initialize_with { new("#{username}@#{host}") }
  end
end

我有规范 (spec/models/user_spec.rb):

RSpec.describe User, type: :model do
  # ...

  it { is_expected.to_not allow_value(FactoryGirl.create(:email, :with_blank_host)).for(:email) }
  it { is_expected.to_not allow_value(FactoryGirl.create(:email, :with_blank_username)).for(:email) }
end

这样使用FactoryGirl对吗?这是一种不良做法吗?

假设在没有 host/username 的情况下创建这些电子邮件背后的逻辑将不会在除测试之外的任何其他地方使用。为什么要留在厂里?

为什么不简单地做:

FactoryGirl.define do
  factory :email, class: String do
    skip_create

    transient do
      username 'user'
      subdomain 'mail'
      domain_name 'example.com'

      host { [subdomain, domain_name].compact.join('.') }
    end
    initialize_with { new("#{username}@#{host}") }
  end
end

RSpec.describe User, type: :model do
  # ...

  it 'should not allow blank host' do
    is_expected.to_not allow_value(FactoryGirl.create(:email, host: '')).for(:email)
  end

  it 'should not allow blank username' do
    is_expected.to_not allow_value(FactoryGirl.create(:email, username: '').for(:email)
  end
end

最重要的是,有一个工厂来创建电子邮件字符串真的有意义吗?为什么不简单地使用字符串(这样更简单):

is_expected.to_not allow_value(FactoryGirl.create(:email, host: '')).for(:email)

is_expected.to_not allow_value('test@').for(:email)

如果你想通过测试获得一致的 :email,为什么不把它放在用户中。

FactoryGirl.define do
  factory :user

    transient do
      username 'user'
      subdomain 'mail'
      domain_name 'example.com'

      host { [subdomain, domain_name].compact.join('.') }
    end

    before(:create) do |user, evaluator|
      user.email = "#{username}@#{host}"
    end
  end
end

但是如果逻辑属于那些特定的测试,为什么要在工厂中抽象所有这些而不是简单地使用 before/callback。 根据经验,我只将在工厂规范文件中使用的逻辑放在工厂中,其他一切都在适当的 before/after 回调中

RSpec.describe User, type: :model do
  before :each do
    # logic before each test of the file
  end

  context 'email validation'

    before :each do
      # logic before each test concerning the email validation
      # useful to factor stuff that will be used multiple time
      # e.g.
      #  @email_ok = 'user@example.com'
    end

    it 'should not allow blank host' do
      # test specific setup
      # useful for stuff only used in this test
      # e.g
      #  email_no_host = 'user@'

      # expectation
    end
  end
end

简而言之:

你所做的当然会奏效。这本身并不是坏习惯。恕我直言,这没有多大意义。

已编辑

您还可以在测试范围内添加一个助手,以免使模型太胖:

RSpec.describe User, type: :model do
  context 'email validation'
    def generate_email(**opts)
        options = {username: 'user', subdomain: 'mail', domain_name 'example.com'}.merge(opts)
        username = options[:username]
        host = options[:host] || "#{options[:subdomain]}.#{options[:domain_name]}"

        "#{username}@#{host}"
    end

    it 'should not allow blank host' do
      is_expected.to_not allow_value(generate_email host: '').for(:email)
    end

    # Here goes 100 other use of generate_email
  end
end