Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I set an _id value in FactoryBot that isn't a foreign key?

I'm using FactoryBot (formerly FactoryGirl) to create some factory data for my tests. I have a model that looks like this via the schema (stripped down to just the relevant stuff):

create_table "items", force: :cascade do |t|
    t.text "team"
    t.text "feature_id"
    t.text "feature"
    t.timestamps
end

However, feature_id and feature are NOT references to a feature object... they are just strings.

I've defined my factory like this:

FactoryBot.define do
  factory :item do
    team "TheTeam"
    sequence(:feature_id) {|n| "Feature#{n}" }
    feature { Faker::Lorem.sentence }
  end
end

And the simple case works:

> FactoryBot.create(:item)
=> #<Item:0x007fad8cbfc048
 id: 1,
 team: "TheTeam",
 feature_id: "Feature1",
 feature: "Qui voluptatem animi et rerum et.",
 created_at: Wed, 10 Jan 2018 02:40:01 UTC +00:00,
 updated_at: Wed, 10 Jan 2018 02:40:01 UTC +00:00>

But when I want to specify my own feature_id this is what happens:

> FactoryBot.create(:item, feature_id: "123")
=> #<Item:0x007fad8d30a880
 id: 2,
 team: "TheTeam",
 feature_id: "123",
 feature: nil,
 created_at: Wed, 10 Jan 2018 02:40:59 UTC +00:00,
 updated_at: Wed, 10 Jan 2018 02:40:59 UTC +00:00>

You can see that feature is now nil. I'm assuming this is because it's trying to infer that feature_id and feature are somehow related. But in this case, I don't want them to be.

Is there a better way to define the factory so that it just treats them as unrelated fields?

BTW, if I try to set both the feature_id and feature it looks like this:

> FactoryBot.create(:item, feature_id: "123", feature: "hi")
=> #<Item:0x007fad8d262810
 id: 3,
 team: "TheTeam",
 feature_id: nil,
 feature: nil,
 created_at: Wed, 10 Jan 2018 02:45:01 UTC +00:00,
 updated_at: Wed, 10 Jan 2018 02:45:01 UTC +00:00>

So it just sets both fields to nil. I suspect FactoryBot is trying to be "smart" about these fields based on their names. I'd change them but they are already set that way in the Db.

like image 901
Dan Sharp Avatar asked Jan 10 '18 02:01

Dan Sharp


1 Answers

It does appear that FactoryBot is making assumptions, and I haven't found a way to change those assumptions. It might be worth opening an issue to see what the maintainers have to offer.

In the mean time, here's a workaround:

FactoryBot.define do
  FEATURE_IDS ||= (1..1000).cycle

  factory :item do
    team "TheTeam"

    transient { without_feature_id false }
    transient { without_feature false }

    after(:build, :stub) do |item, evaluator|
      item.feature_id = "Feature#{FEATURE_IDS.next}" unless evaluator.without_feature_id
      item.feature = Faker::Lorem.sentence unless evaluator.without_feature
    end
  end
end

This will function properly in the cases you described above.

The incrementing is tricky. I was not able to find a way to use FactoryBot sequences outside of the resource-construction context, so I use an Enumerator and call #next to create the sequence. This works similar to a FactoryBot sequence, except that there is no way to reset to 1 in the middle of a test run.

RSpec tests prove it works as expected, whether we are creating items in the database or building them in memory:

context 'when more than one item is created' do
  let(:item_1) { create(:item) }
  let(:item_2) { create(:item) }

  it 'increments feature_id by 1' do
    expect(item_1.feature_id).to be_present
    expect(item_2.feature_id).to eq(item_1.feature_id.next)
  end
end

context 'when using build instead of create' do
  let(:item_1) { build(:item) }
  let(:item_2) { build(:item) }

  it 'increments feature_id by 1' do
    expect(item_1.feature_id).to be_present
    expect(item_2.feature_id).to eq(item_1.feature_id.next)
  end
end

Note that you cannot create an item without a feature_id or a feature using the typical construct; for example:

>> item = create(:item, feature_id: nil)

Will result in

>> item.feature_id
#> "Feature1"

If you wish to create an object without the feature and feature_id fields, you can do:

create(:item, without_feature_id: true, without_feature: true)
like image 131
moveson Avatar answered Nov 04 '22 14:11

moveson