Beartech
Beartech

Reputation: 6411

Why is my Rspec test failing with duplicate name when name not unique?

I have a model with these validations:

class Tool < ActiveRecord::Base
  has_many :repairs
  has_many :services
  belongs_to :category
  belongs_to :location
  accepts_nested_attributes_for :repairs
  accepts_nested_attributes_for :services

  validates :name, :serial, :model, :presence => true
  validates_uniqueness_of :serial
end

My factory look like this:

require 'faker'

FactoryGirl.define do
  factory :tool do
    association :location
    association :category
    name {Faker::Name.first_name}
    model {Faker::Company.name}
    serial {Faker::Address.latitude}
  end
end

And I have some simple tests like these:

it 'is valid with a name, serial, model, location, category' do

  expect(build(:tool, location: Location.new(name: 'Sta 72'),
               category: Category.new(name: 'Chainsaw'))).to be_valid

end

it 'is invalid without a name' do
  expect(build(:tool, name: nil)).to have(1).errors_on(:name)
end

it 'is invalid without a serial' do
  expect(build(:tool, serial: nil)).to have(1).errors_on(:serial)
end

it 'is invalid without a model' do
  expect(build(:tool, model: nil)).to have(1).errors_on(:model)
end

it 'is invalid with a duplicate serial' do
   tool1 = create(:tool, serial: '12345')
   tool2 = build(:tool, serial: tool1.serial)

  expect(tool2).to have(1).errors_on(:serial)
end

Everything works perfect except the last test that checks to make sure :serial is unique. It fails with this:

ActiveRecord::RecordInvalid: Validation failed: Name has already been taken
./spec/models/tool_spec.rb:27:in `block (3 levels) in <top (required)>'

I don't know why it complains about Name when I'm not validating uniqueness of Name. Line 27 refers to tool2 = build(:tool, serial: tool1.serial)

edit* schema.rb looks like this:

...
  create_table "tools", force: true do |t|
    t.string   "name"
    t.string   "serial"
    t.date     "purchased"
    t.date     "put_in_service"
    t.decimal  "cost"
    t.decimal  "value"
    t.boolean  "in_service"
    t.date     "retired"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.text     "note"
    t.integer  "condition"
    t.text     "old_location"
    t.string   "model"
    t.boolean  "loaner",         default: false
    t.integer  "location_id"
    t.integer  "category_id"
  end

  add_index "tools", ["category_id"], name: "index_tools_on_category_id", using: :btree
  add_index "tools", ["location_id"], name: "index_tools_on_location_id", using: :btree

my test log looks like this when I run just the test that's failing:

 ActiveRecord::SchemaMigration Load (0.6ms)  SELECT "schema_migrations".* FROM "schema_migrations"
 (0.4ms)  BEGIN
 (0.3ms)  SAVEPOINT active_record_1
SQL (9.3ms)  INSERT INTO "locations" ("created_at", "name", "type", "updated_at", "vehicle") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["created_at", Mon, 27 Jan 2014 22:28:52 PST -08:00], ["name", "Loaners"], ["type", "Station"], ["updated_at", Mon, 27 Jan 2014 22:28:52 PST -08:00], ["vehicle", true]]
 (0.3ms)  RELEASE SAVEPOINT active_record_1
 (0.3ms)  SAVEPOINT active_record_1
 Category Exists (1.5ms)  SELECT 1 AS one FROM "categories" WHERE "categories"."name" = 'Chainsaws' LIMIT 1
 SQL (1.2ms)  INSERT INTO "categories" ("created_at", "name", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["created_at", Mon, 27 Jan 2014 22:28:52 PST -08:00], ["name", "Chainsaws"], ["updated_at", Mon, 27 Jan 2014 22:28:52 PST -08:00]]
 (0.2ms)  RELEASE SAVEPOINT active_record_1
 (0.4ms)  SAVEPOINT active_record_1
 Tool Exists (1.0ms)  SELECT 1 AS one FROM "tools" WHERE "tools"."serial" = '12345' LIMIT 1
 SQL (1.4ms)  INSERT INTO "tools" ("category_id", "created_at", "location_id", "model", "name", "serial", "updated_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"  [["category_id", 77], ["created_at", Mon, 27 Jan 2014 22:28:52 PST -08:00], ["location_id", 43], ["model", "Stark LLC"], ["name", "Cristina"], ["serial", "12345"], ["updated_at", Mon, 27 Jan 2014 22:28:52 PST -08:00]]
 (0.2ms)  RELEASE SAVEPOINT active_record_1
 (0.3ms)  SAVEPOINT active_record_1
 SQL (0.9ms)  INSERT INTO "locations" ("created_at", "name", "type", "updated_at", "vehicle") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["created_at", Mon, 27 Jan 2014 22:28:52 PST -08:00], ["name", "Loaners"], ["type", "Station"], ["updated_at", Mon, 27 Jan 2014 22:28:52 PST -08:00], ["vehicle", true]]
 (0.3ms)  RELEASE SAVEPOINT active_record_1
 (0.3ms)  SAVEPOINT active_record_1
 Category Exists (0.5ms)  SELECT 1 AS one FROM "categories" WHERE "categories"."name" = 'Chainsaws' LIMIT 1
 (0.3ms)  ROLLBACK TO SAVEPOINT active_record_1
 (0.6ms)  ROLLBACK

EDIT->

Thanks to the answers below my problem was solved. The code I used based on their answer is:

it 'is invalid with a duplicate serial' do
  chainsaw = create(:category, name: 'Chainsaw')
   tool1 = create(:tool, serial: '12345', category: chainsaw)
   tool2 = build(:tool, serial: '12345', category: chainsaw)

  expect(tool2).to have(1).errors_on(:serial)
end

What I don't understand is why FactoryGirl and Faker wouldn't create the two tools and honor the validates uniqueness constraint on the Category model. Shouldn't it either just reuse the Category it just created for the first tool (rather than try to create a new on with the same name) or create a new Category for the new tool that is different than the previous one?

aha! this is why:

FactoryGirl.define do
  factory :category do
    name 'Chainsaws'
  end
end

I'll set this factory to use Faker and it should work as I was expecting.

Upvotes: 1

Views: 1886

Answers (2)

Peter Alfvin
Peter Alfvin

Reputation: 29389

It looks like you're getting a validation error on the implicit creation of the Category instance, not of the Tools instance.

Since it's occurring on the build(:tool, ...) call, we know that the Category instance implicitly created to support the build conflicts with a previously created Category instance. From the log you shared, we can see that both the preceding create(:tool, ...) call and the build(:tool, ...) are both creating Category instances with a name of Chainsaws.

(Note: An earlier version of this answer mistakenly hypothesized a conflict with the first example, but that didn't make sense since the name in the first example was Chainsaw rather than Chainsaws, not to mention the likelihood that transactions are most likely enabled, protecting the last example from the impact of the first.)

Upvotes: 3

Derek Prior
Derek Prior

Reputation: 3507

The uniqueness constraint being tripped is likely on Category#name. Are you setting that to a static value in your factory?

# This causes the associated category to be created
tool1 = create(:tool, serial: '12345')

# The build strategy actually creates associated records.
# This is trying to create a category with a duplicate name.
tool2 = build(:tool, serial: tool1.serial)

If you update your Category factory to use a sequence or fake data for name, this should work.

Upvotes: 1

Related Questions