I am writing tests for my models and came across an error I can't seem to resolve. I'm using rspec and Fabricator. Things work fine when being tested in isolation but when I try to test for associations I get an ActiveModel::MissingAttributeError.
models/user.rb
class User < ApplicationRecord
...
validates :email, uniqueness: true, presence: true
belongs_to :company, required: false
end
models/company.rb
class Company < ApplicationRecord
validates :organisation_number, uniqueness: true, presence: true
has_many :users, dependent: :destroy
end
schema
create_table "companies", force: :cascade do |t|
t.string "organisation_number"
...
end
create_table "users", id: :serial, force: :cascade do |t|
t.string "email", default: "", null: false
...
t.bigint "company_id"
t.index ["company_id"], name: "index_users_on_company_id"
...
end
fabricators/user_fabricator.rb
Fabricator :user do
email { Faker::Internet.email }
password '123456'
confirmed_at Time.now
end
fabricators/company_fabricator.rb
Fabricator :company do
organisation_number { Faker::Company.swedish_organisation_number }
end
spec/user_spec.rb (first test passes, the second fails)
describe User do
context '#create' do
it 'Creates a user when correct email and password provided' do
user = Fabricate(:user)
expect(user).to be_valid
end
it 'Lets assign a company to user' do
company = Fabricate(:company)
expect(Fabricate.build :user, company: company).to be_valid
end
end
end
I also tried adding a company straight to the User fabricator, like so (which seems to me like a correct implementation of the documentation):
Fabricator :user do
email { Faker::Internet.email }
password '123456'
confirmed_at Time.now
company
end
and the other way around, adding users to the Company fabricator, like so:
Fabricator :company do
organisation_number { Faker::Company.swedish_organisation_number }
users(count: 3) { Fabricate(:user) }
end
but both approaches left me with the same error:
User#create Lets assign a company to user
Failure/Error: company = Fabricate(:company)
ActiveModel::MissingAttributeError: can't write unknown attribute 'company_id'
Any suggestions on what I'm doing wrong?
I would write up my own answer but I am not sure that I could describe it better than RSpec already has so this is taken directly from Here:
Rails 4.x ActiveRecord::Migration pending migration checks
If you are not using ActiveRecord you do not need to worry about these
settings.
Users of Rails 4.x can now take advantage of improved schema migration and sync abilities. Prior to RSpec 3, users were required to manually run migrations in both the development and test environments. Additionally, the behavior differed depending on if the specs were run via rake or via the standalone rspec command.
With the release of Rails 4, new APIs have been exposed on
ActiveRecord::Migration. This allows RSpec to take advantage of these new
standard migration checks, mirroring behavior across the board.
Rails 4.0.x
Add the following to the top of the rails_helper file after Rails has
been required:
ActiveRecord::Migration.check_pending!
This will raise an exception if there are any pending schema changes. Users will still be required to manually keep the development and test environments in sync.
Rails 4.1+
With this release there was an exciting new feature. Users no longer need to keep the development and test environments in sync. To take advantage of this add the following to the top of the rails_helper file after Rails has been required:
ActiveRecord::Migration.maintain_test_schema!
What this does is that rather than just raising when the test schema has
pending migrations, Rails will try to load the schema. An exception will
now only be raised if there are pending migrations afterwards the schema
has been loaded.
There are a few caveates to be aware of when using this:
Migrations still need to be run manually; although now this only has to be done in the 'development' environment
An exception will be raised If the schema has not been initialized. The exception will provide instructions stating rake db:migrate needs to be run.
It is possible to opt-out of checking for pending migrations. Since this is
actually a feature of Rails, the change needs to be done as part of the Rails
configuration. To do this, add the following to your config/environments/test.rb file:
config.active_record.maintain_test_schema = false
New RSpec projects don't need to worry about these commands as the rails generate rspec:install will add them automatically.
Related
In my model, I have:
class Listing < ApplicationRecord
...
has_rich_text :description
...
end
In my seeds.rb:
#listing = Listing.new(
title: 'CODE VEIN',
price: 59.99 * 100,
description: "<p>#{Faker::Lorem.paragraphs(number: 30).join(' ')}</p>",
seller_id: seller.id,
release_date: Date.parse('Sep 26, 2019'),
status: :active,
esrb: 'MATURE'
)
Listing.description comes up nil, causing my NOT NULL constraint to error.
I've debugged with pry, and tried #listing.description.body= text or #listing.description.body = ActionText::Content.new(text), both still cause the listing#description to be nil.
This is an API Only project, but I use Trix RTE in the front-end react app. Is there a specific method to seed rich_text columns?
ActionText stores the actual contents in a seperate action_text_rich_texts table which uses a polymorphic assocation to link back to the model that its attached to.
# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false
t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
end
end
The JavaScript component of ActionText (which is really the whole point) automatically sends AJAX requests when the user user interacts with Trix to create/update the row in the action_text_rich_texts table even before you have saved the record you're creating.
When you then submit the form you're actually submitting the id to the row on the action_text_rich_texts table and not the contents. Then when you save your model it updates the corresponding row on action_text_rich_texts with the record_type and record_id from the model. Its a pretty awkward solution that is built around the idea of never having to add columns to your model.
I don't really see the point in using ActionText in an API as the whole point is to add a quick and dirty Trix implementation to classical rails apps. You really should be able to solve this with just a single text column instead. Especially as that will let you access the contents without joining.
There must be something else going on with your code. Maybe if you share your migration and the complete Listing class file it might be easier to spot what's going on.
Here are a few steps to make sure you got it right:
Create a brand new rails app (you can delete it later):
rails new testrichtext -d mysql --api
Create the db
cd testrichclient
rake db:create
Create the model
rails g model listing description:text
Change your newly created migration file to make sure the column is not null:
class CreateListings < ActiveRecord::Migration[6.0]
def change
create_table :listings do |t|
t.text :description, null: false
t.timestamps
end
end
end
Run the migration
rake db:migrate
Now you should be able to log in into the console, and create a new listing with something as the description:
rails c
And inside console:
l = Listing.new(description: "<p>Something nice</p>")
l.save!
Listing.first.description
As you can see, this is enough to save/seed a new listing with rich text. So anything you may have going on there should be something you're causing somewhere else, by adding a validation differently, or callbacks. Hard to say without looking at the entire file
I found this to work for me:
15.times do
Post.create!(title: Faker::Book.unique.title)
end
then
Post.all.each do |post|
ActionText::RichText.create!(record_type: 'Post', record_id: post.id, name: 'content', body: Faker::Lorem.sentence)
end
To read:
Post.first.content.body.to_plain_text
or:
Post.first.content.body.to_trix_html
...as stated above by OP.
Source:
https://linuxtut.com/rails6-input-the-initial-data-of-actiontext-using-seed-9b4f2/
The project is to build a web-app which I will use to log and track my strength training sessions. One of the tables/models is a join table linking exercises and trained bodyparts.
Here is the migration for the exercise table:
# migration: successfully creates the join table
class CreateExerciseBodyparts < ActiveRecord::Migration[5.2]
def change
create_table :exercise_bodyparts do |t|
t.references :exercise, foreign_key: true
t.references :bodypart, foreign_key: true
t.timestamps
end
end
end
Here is the seed data for the table:
# seed data snippet: no syntax errors here
def exercise_bodyparts_list
return [
{
name: "Barbell Back Squats",
bodyparts: ["Quads","Hamstrings","Adductors","Abductors","Glutes","Lower Back","Abs"]
},
{
name: "Barbell Biceps Curls ~0°",
bodyparts: ["Biceps","Forearms"]
},
{
name: "Barbell Biceps Curls ~45°",
bodyparts: ["Biceps","Forearms"]
},
#(...)
]
end
Here is my seed method to populate the DB:
# seed method: where the error comes...
exercise_bodyparts_list.each do |ex_body|
ex_body[:bodyparts].each do |bodypart|
ExerciseBodypart.create!(
exercise: Exercise.find_by_name(ex_body[:name]),
bodypart: Bodypart.find_by_name(bodypart)
)
end
end
Here is the error message:
# error message:
ActiveModel::UnknownAttributeError: unknown attribute 'exercise' for ExerciseBodypart.
I do not know exact reason but it was working for me inside rake task in rails-3 but failed for rails-5 while updating application to higher version.
I suggest you try following, following worked for me,
exercise_id: Exercise.find_by_name(ex_body[:name]).id
I received advice from another Ruby developer who said that I ought to be naming the relationship nature in the model with belongs_to
I indeed did that and the logic works ok.
My model looks like this:
class ExerciseBodypart < ApplicationRecord
belongs_to :exercise
belongs_to :bodypart
end
Lo and behold, this was the key. This espressed relationship in the model doesn't exist in my other project; however I suspect that the relationship is taken from the connection of other models - as it is a more complicated database with many more models.
I am using Ruby Workflow in my ActiveRecords using Gem: Workflow
Existing Running Code contains:
I am having an ActiveRecord: X
I am having two Migrations already:
(Ref1) CreateX migration (which creates table X)
(Ref2) CreateInitialEntryInX migration (which creates one entry in table X)
New Changes:
Now I wanted to add workflow in ActiveRecord X, hence I did:
(Ref3) I added the workflow code in ActiveRecord Model X (mentioning :status as my workflow field)
(Ref4) AddStatusFieldToX migration (which adds :status field in table X)
Now when I run rake db:migrate after the changes, the (Ref2) breaks cos Migration looks for :status field as it is mentioned in ActiveRecord Model in the Workflow section, but :status field has not been added yet as migration (Ref4) has not executed yet.
Hence, all the builds fail when all migrations are run in sequence, Any solution to this?
I do not want to resequence any of the migration or edit any old existing migrations.
My Model looks like:
class BaseModel < ActiveRecord::Base
#
# Workflow to define states of Role
#
# Initial State => Active
#
# # State Diagram::
# Active --soft_delete--> Deleted
# Deleted
#
# * activate, soft_delete are the event which triggers the state transition
#
include Workflow
workflow_column :status
workflow do
state :active, X_MODEL_STATES::ACTIVE do
event :soft_delete, transitions_to: :deleted
end
state :deleted, X_MODEL_STATES::DELETED
on_transition do |from, to, event, *event_args|
self.update_attribute(:status, to)
end
end
def trigger_event(event)
begin
case event.to_i
when X_MODEL_EVENTS::ACTIVATE
self.activate!
when X_MODEL_EVENTS::SOFT_DELETE
self.soft_delete!
end
rescue ....
end
end
class X_MODEL_STATES
ACTIVE = 1
DELETED = 2
end
class X_MODEL_EVENTS
ACTIVATE = 1
SOFT_DELETE = 2
end
# Migrations(posting Up functions only - in correct sequence)
#--------------------------------------------------
#1st: Migration - This is already existing migration
CreateX < ActiveRecord::Migration
def up
create_table :xs do |t|
t.string :name
t.timestamps null: false
end
end
end
#2nd: Migration - This is already existing migration
CreateInitialX < ActiveRecord::Migration
def up
X.create({:name => 'Kartik'})
end
end
#3rd: Migration - This is a new migration
AddStatusToX < ActiveRecord::Migration
def up
add_column :xs, :status, :integer
x.all.each do |x_instance|
x.status = X_MODEL_STATES::ACTIVE
x.save!
end
end
end
So, when Migration#2 runs, it tries to find :status field to write is with initial value of X_MODEL_STATES::ACTIVE as it is mentioned in Active Record Model files workflow as: workflow_column :status and does not find the field as Migration#3 is yet to execute.
You can wrap up your workflow code by a check for column_name.
if self.attribute_names.include?('status')
include Workflow
workflow_column :status
workflow do
...
end
end
This will result in running workflow code only after 'AddStatusToTable' migration ran successfully.
This is a pain, as your models need to be consistent for migrations. I don't know any auto solutions for this.
Theoreticaly the best way would be to have model code versions binded with migrations. But I don't know any system that allows this.
Every time I do big models refactoring i experience this problem. Possible solutions to this situation.
Run migrations on production manually to assure consitent state between migrations and models
Temporarily comment out workflow code in model to run blocking migration, then deploy, then uncomment workflow code and move on with deploy and next migrations
Version migrations and model changes on branches, so they are consistent. Deploy to production and run migration by chunks
Include temp workarounds in the model code, and remove them from source after migrations on production are deployed.
Monkey-patch model in migration code for backwards compatibility. In your situation this would be to dynamically remove 'workflow' from model code, which might be hard, and then run migration
All solutions are some kind of dirty hacks, but it's not easy to have migrations and model code versioned. The best way would be to deploy in chunks or if need to deploy all at once use some temp code in model and remove it after deploy on production.
THANKS ALL
I found the solution to this, and i am posting it here. The problems to issue here were:
Add :status field to ActiveRecord Model X without commenting out Workflow Code and not let Workflow disallow creation of an instance in Table X during migration.
Secondly, add an if condition to it as specified by #007sumit.
Thirdly, to be able to reload Model in migration with the updated column_information of Model X
Writing whole code solution here:
class BaseModel < ActiveRecord::Base
#
# Workflow to define states of Role
#
# Initial State => Active
#
# # State Diagram::
# Active --soft_delete--> Deleted
# Deleted
#
# * activate, soft_delete are the event which triggers the state transition
#
# if condition to add workflow only if :status field is added
if self.attribute_names.include?('status')
include Workflow
workflow_column :status
workflow do
state :active, X_MODEL_STATES::ACTIVE do
event :soft_delete, transitions_to: :deleted
end
state :deleted, X_MODEL_STATES::DELETED
end
end
def trigger_event(event)
...
end
end
class X_MODEL_STATES
...
end
class X_MODEL_EVENTS
...
end
# Migrations(posting Up functions only - in correct sequence)
#--------------------------------------------------
#1st: Migration - This is already existing migration
CreateX < ActiveRecord::Migration
def up
create_table :xs do |t|
t.string :name
t.timestamps null: false
end
end
end
#2nd: Migration - This is already existing migration
CreateInitialX < ActiveRecord::Migration
def up
X.create({:name => 'Kartik'})
end
end
#3rd: Migration - This is a new migration to add status field and
# modify status value in existing entries in X Model
AddStatusToX < ActiveRecord::Migration
def up
add_column :xs, :status, :integer
# This resets Model Class before executing this migration and
# Workflow is identified from the if condition specified which was
# being skipped previously without this line as Model Class is
# loaded only once before all migrations run.
# Thanks to post: http://stackoverflow.com/questions/200813/how-do-i-force-activerecord-to-reload-a-class
x.reset_column_information
x.all.each do |x_instance|
x.status = X_MODEL_STATES::ACTIVE
x.save!
end
end
end
#stan-brajewski Now, this code can go in one deployment.
Thanks all for inputs :)
I've got a model Foo which has state_code as a foreign key. The States table is a (more or less) static table created to hold the codes and names for the 50 states, as well as other US postal codes (e.g. "PR" for Puerto Rico). I opted to use state_code as the primary key on States and as the foreign key on Foo, rather than something like state_id. It reads better to humans, and simplifies view logic where I want to call the state code. (EDIT - just to clarify: I don't mean calling code to access the model from the view; I mean that displaying the state as #foo.state_code seems simpler than #foo.state.state_code.)
Foo also has a has_many relationship with model Bar. Both model specs pass a spec for valid factories but for some reason when running a feature spec that builds an instance of Bar, the test blows up due to a foreign key issue related to state_code
I get passing model specs for all of my models, including the test for a valid factory. However, I'm running into trouble whenever I try to create a test object for 'Bar'. Using build blows up on a foreign key error for state_code in Foo (despite fact that the Foo factory explicitly specifies a value that is confirmed to exist as a state_code in States). Using build_stubbed for the Bar object doesn't seem to persist the object.
The models:
# models/foo.rb
class Foo < ActiveRecord
belongs_to :state, foreign_key: 'state_code', primary_key: 'state_code'
has_many :bars
validates :state_code, presence: true, length: { is: 2 }
# other code omitted...
end
# models/state.rb
class State < ActiveRecord
self.primary_key = 'state_code'
has_many :foos, foreign_key: 'state_code'
validates :state_code, presence: true, uniqueness: true, length: { is: 2 }
# other code omitted...
end
# models/bar.rb
class Bar < ActiveRecord
belongs_to :foo
# other code omitted
end
The factory below passes green for my Foo and Bar models, so from the model point of view the factories seem fine:
# spec/factores/foo_bar_factory.rb
require 'faker'
require 'date'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state_code { 'AZ' } # Set code for Arizona b/c doesn't matter which state
end
factory :bar do
name { Faker::Name.name }
website_url { Faker::Internet.url }
# other columns omitted
association :foo
end
end
...where the basic specs are:
# spec/models/foo_spec.rb
require 'rails_helper'
describe Foo, type: :model do
let(:foo) { build(:foo) }
it "has a valid factory" do
expect(foo).to be_valid
end
# code omitted...
end
# spec/models/bar_spec.rb
require 'rails_helper'
describe Bar, type: :model do
let(:bar) { build_stubbed(:bar) } # have to build_stubbed - build causes error
it "has a valid factory" do
expect(bar).to be_valid
end
end
This spec passes, with no issues. But if I use build(:bar) for Bar instead of build_stubbed, I get an error on foreign key:
1) Bar has a valid factory
Failure/Error: let(:bar) { build(:bar) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "bars" violates foreign key constraint "fk_rails_3dd3a7c4c3"
DETAIL: Key (state_code)=(AZ) is not present in table "states".
The code 'AZ' is definitely in the states table, so I'm unclear why it fails.
In a feature spec I'm attempting to create instances of bar that persist in the database, so I can test they are appearing correctly in #index, #show, and #edit actions. However I can't seem to get it working correctly. The feature spec fails:
# spec/features/bar_pages_spec.rb
require 'rails_helper'
feature "Bar pages" do
context "when signed in as admin" do
let!(:bar_1) { build_stubbed(:bar) }
let!(:bar_2) { build_stubbed(:bar) }
let!(:bar_3) { build_stubbed(:bar) }
# code omitted...
scenario "clicking manage bar link shows all bars" do
visit root_path
click_link "Manage bars"
save_and_open_page
expect(page).to have_css("tr td a", text: bar_1.name)
expect(page).to have_css("tr td a", text: bar_2.name)
expect(page).to have_css("tr td a", text: bar_3.name)
end
end
This spec fails with a message indicating no matches. Using save_and_open_page doesn't show the expected items in the view. (I have a working page with development data though, so I know that the logic actually works as expected). The thoughtbot post on build_stubbed indicates that it should persist objects:
It makes objects look look like they’ve been persisted, creates
associations with the build_stubbed strategy (whereas build still uses
create), and stubs out a handful of methods that interact with the
database and raises if you call them.
...but it doesn't appear to be doing so in my spec. Attempting to use build in lieu of build_stubbed in this spec generates the same foreign key error noted above.
I'm really stuck here. The models appear to have valid factories and pass all specs. But feature specs either blow up the foreign key relationship or don't seem to persist the build_stubbed object between views. It feels like a mess but I can't figure out the right approach to fix it. I have actual, working views in practice, that do what I expect - but I'd like to have test coverage that works.
UPDATE
I went back and updated all of the model code to remove the natural key for state_code. I followed all of #Max's recommendations. The Foo table now uses state_id as the foreign key for states; I copied in the code for app/models/concerns/belongs_to_state.rb as recommended, etc.
Updated schema.rb:
create_table "foos", force: :cascade do |t|
# columns omitted
t.integer "state_id"
end
create_table "states", force: :cascade do |t|
t.string "code", null: false
t.string "name"
end
add_foreign_key "foos", "states"
The model specs passed, and some of my simpler feature specs passed. I now realize that the problem is only when more than one Foo object gets created. When this happens, the second object fails due to the uniqueness constraint on the column :code
Failure/Error: let!(:foo_2) { create(:foo) }
ActiveRecord::RecordInvalid:
Validation failed: Code has already been taken
I've tried to set the :state_id column directly in the factory for :foo to avoid calling the :state factory. E.g.
# in factory for foo:
state_id { 1 }
# generates following error on run:
Failure/Error: let!(:foo_1) { create(:foo) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "foos" violates foreign key constraint "fk_rails_5f3d3f12c3"
DETAIL: Key (state_id)=(1) is not present in table "states".
Obviously state_id isn't in states, since it's id on states, and state_id in foos. Another approach:
# in factory for foo:
state { 1 } # alternately w/ same error -> state 1
ActiveRecord::AssociationTypeMismatch:
State(#70175500844280) expected, got Fixnum(#70175483679340)
Or:
# in factory for foo:
state { State.first }
ActiveRecord::RecordInvalid:
Validation failed: State can't be blank
All I really want to do is create an instance of the Foo object and have it include the relationship to one of the states from the states table. I don't anticipate doing a lot of changes to the states table - it's really just a reference.
I DON'T need to create a new state. I just need to populate the foreign key state_id on the Foo object with one of the 66 values in the :id column on the states table. Conceptually, the factory for :foo would ideally just pick an integer value between 1 and 66 for the :state_id. It works in console:
irb(main):001:0> s = Foo.new(name: "Test", state_id: 1)
=> #<Foo id: nil, name: "Test", city: nil, created_at: nil, updated_at: nil, zip_code: nil, state_id: 1>
irb(main):002:0> s.valid?
State Load (0.6ms) SELECT "states".* FROM "states" WHERE "states"."id" = $1 LIMIT 1 [["id", 1]]
State Exists (0.8ms) SELECT 1 AS one FROM "states" WHERE ("states"."code" = 'AL' AND "states"."id" != 1) LIMIT 1
=> true
Only way forward I can see right now is to get rid of the uniqueness constraint on :code column in states. Or - remove the foreign key constraint between foos and states, and let Rails enforce the relationship.
Sorry for the massive post...
I'm going to be a pain in the *rse and argue that conventions might be more important than developer convenience and perceived readability.
One of the great things with Rails is that the strong conventions allow us to open up any project and figure out what is going on pretty fast (provided the original author is not a total hack). Try that with a PHP project.
One of these conventions is that foreign keys are postfixed with _id. Many other components such as FactoryGirl rely on these conventions.
I would also argue that using the state code as a primary ID will cause issues if your app ever finds use beyond the US. What happens when you need to keep track of Canadian provinces or Indian states and territories? How are you going to deal with the unavoidable conflicts? Even if you think that this might not be the deal today remember that requirements change with time.
I would model it as:
create_table "countries", force: :cascade do |t|
t.string "code", null: false # ISO 3166-1 alpha-2 or alpha-3
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "countries", ["code"], name: "index_countries_on_code"
create_table "states", force: :cascade do |t|
t.integer "country_id"
t.string "code", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "states", ["code"], name: "index_states_on_code"
add_index "states", ["country_id", "code"], name: "index_states_on_country_id_and_code"
add_index "states", ["country_id"], name: "index_states_on_country_id"
"and simplifies view logic where I want to call the state code"
I would argue that you should not be doing database calls at all from your views if it is avoidable. Query upfront from your controller and pass data to your views. It makes it much simpler to optimise queries and avoid N+1 issues.
Use presenters or helper methods to help manage complexity. The slight inconvenience of having to do State.find_by(code: 'AZ') instead of State.find('AZ') is most likely not as important as you think.
added:
This is how you would use associations properly in FactoryGirl. Consider the simplicity in this solution a final argument why your custom foreign key arrangement may be causing more grief than convenience.
models:
class State < ActiveRecord::Base
# Only the State model should be validating its attributes.
# You have a major violation of concerns.
validates_uniqueness_of :state_code
validates_length_of :state_code, is: 2
end
# app/models/concerns/belongs_to_state.rb
module BelongsToState
extend ActiveSupport::Concern
included do
belongs_to :state
validates :state, presence: true
validates_associated :state # will not let you save a Foo or Bar if the state is invalid.
end
def state_code
state.state_code
end
def state_code= code
self.assign_attributes(state: State.find_by!(state_code: code))
end
end
class Foo < ActiveRecord::Base
include BelongsToState
end
class Bar < ActiveRecord::Base
include BelongsToState
end
Factories:
# spec/factories/foos.rb
require 'faker'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state
end
end
# spec/factories/states.rb
FactoryGirl.define do
factory :state do
state_code "AZ"
name "Arizona"
end
end
These specs use shoulda-matchers for the extremely succint validation examples:
require 'rails_helper'
RSpec.describe Foo, type: :model do
let(:foo) { build(:foo) }
it { should validate_presence_of :state }
it 'validates the associated state' do
foo.state.state_code = 'XYZ'
foo.valid?
expect(foo.errors).to have_key :state
end
describe '#state_code' do
it 'returns the state code' do
expect(foo.state_code).to eq 'AZ'
end
end
describe '#state_code=' do
let!(:vt) { State.create(state_code: 'VT') }
it 'allows you to set the state with a string' do
foo.state_code = 'VT'
expect(foo.state).to eq vt
end
end
end
# spec/models/state_spec.rb
require 'rails_helper'
RSpec.describe State, type: :model do
it { should validate_length_of(:state_code).is_equal_to(2) }
it { should validate_uniqueness_of(:state_code) }
end
https://github.com/maxcal/sandbox/tree/31773581
Also, in your feature, controller or integration specs you need to use FactoryGirl.create not build_stubbed. build_stubbed does not persist models to the database and in these cases you need your controllers to be able to load the records from the database.
Also you should avoid using CSS selectors in your feature specs if possible. Feature specs should describe your application from a user's POV.
feature "Bar management" do
context "as an Admin" do
let!(:bars){ 3.times.map { create(:bar) } }
background do
visit root_path
click_link "Manage bars"
end
scenario "I should see all the bars on the management page" do
# just testing a sampling is usually good enough
expect(page).to have_link bars.first.name
expect(page).to have_link bars.last.name
end
scenario "I should be able to edit a Bar" do
click_link bars.first.name
fill_in('Name', with: 'Moe´s tavern')
# ...
end
end
end
There was a lot going on here, but with respect to the FactoryGirl issue blowing up on the foreign key relationship between Foo and State, I've figured it out.
#Max was spot on about the problem with using a natural key for the primary key on the states table. It doesn't follow Rails convention, and led to some mixing of concerns, such as potentially having to validate the foreign key (e.g. length 2) on the Foo table.
But even after fixing that to link the tables on a Rails-friendly key (:state_id as foreign key on foos, and :id as primary key on states) -- I still could not find any way to create more than a single instance of a Foo object using the :foo factory. It either failed when I tried to "plug" an integer value into state_id, or the :state factory would fail on the second instance, stating that the code already existed. (See my update in the question for details on the attempts and related fails).
The only way around seemed to be removing the uniqueness validation on State, or eliminating the foreign key relationship at the database layer (Postgres 9.4). I decided I didn't want to do the former. And in thinking about the latter, I realized that I really don't need the foreign key constraint in the database. The states table was intended just to provide a consistent list of state codes as a reference point. If I were to delete this table for some reason, it's not true that I'd want to destroy all of the Foo records. They essentially stand alone, with the state just an attribute of Foo. I briefly considered putting the state info into a constant, but meh.
Deleting the database-level foreign key constraint fixed things for me.
bin/rails g migration RemoveForeignKeyStatesFromFoos
class RemoveForeignKeyStatesFromFoos < ActiveRecord::Migration
def change
remove_foreign_key :foos, :states
end
end
This left the :state_id column intact on my foos table, but removed the line add_foreign_key "foos", "states" from my schema.rb
bin/rails g migration AddIndexToStateIdInFoos
class AddIndexToStateIdInFoos < ActiveRecord::Migration
def change
add_index :foos, :state_id
end
end
...added the line add_index "foos", ["state_id"], name: "index_foos_on_state_id", using: :btree to my schema.
After migrating both, I initially made the mistake of deleting the :state factory, thinking that I didn't need to create new states. After some headaches in test I realized that the test database isn't normally seeded with rake db:seed - so my tests were failing due to strange errors for Module::DelegationError. Rather than build a script to seed the test dB with states, I just modified the factory, and kept the association on the :foo factory.
# spec/factories/foo_factory.rb
FactoryGirl.define do
factory :foo do
# columns omitted
state
end
factory :state do
code { Faker::Address.state_abbr }
code { Faker::Address.state }
end
end
At this point, Rails still successfully validates the has_many and belongs_to relationships in the model (which were unchanged).
I understand the add_foreign_key method is relatively new to Rails, as of 4.2. I bit off on it by conflating the fact of the relationship with the need to establish an actual foreign key constraint at the database layer.
From the Rails Guide for ActiveRecord Associations:
You are responsible for maintaining your database schema to match your
associations. In practice, this means two things, depending on what
sort of associations you are creating. For belongs_to associations you
need to create foreign keys, and for has_and_belongs_to_many
associations you need to create the appropriate join table.
The use of the term "foreign keys" in this case appears to mean a different thing for Rails, versus Postgres. Rails seems perfectly happy so long as there is a column in the belongs_to table that matches the convention [parent_table_name]_id. This can be achieved by explicitly adding the column or by using references in a migration:
Using t.integer :supplier_id makes the foreign key naming obvious and
explicit. In current versions of Rails, you can abstract away this
implementation detail by using t.references :supplier instead
In my case, this was plenty sufficient -- an actual foreign key was not necessary.
I am having a problem where RSpec is crashing out when I endeavour to run a model test on the app I have recently inherited.
My test database is configured like this:
test:
adapter: sqlite3
database: ":memory:"
The Schema looks like this:
create_table "categories", force: true do |t|
t.string "name"
t.integer "event"
t.boolean "deleted", default: false, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
My "Category" class looks like this:
class Category < ActiveRecord::Base
# couple of method calls but no attributes.
end
The Spec file begins like this:
describe "Category Model" do
let(:category) { Category.new }
# doesn't matter, crashes after this line.
The outcome is:
/var/lib/gems/1.9.1/gems/activerecord-4.1.0/lib/active_record/connection_adapters/sqlite3_adapter.rb:512:in `table_structure': Could not find table 'categories' (ActiveRecord::StatementInvalid)
Every other question on this topic I can find seems to suggest that there is a problem with the rake db preparation, but this happens regardless of whether I run rake db:test:prepare or rake db:test:load ahead of time.
Is there any way to check what is in that database? Should I be forcing it to create a file instead, so I can see what that looks like? Am I missing something else obvious that could be causing this problem?
Edited to add: Also, given that db:test:prepare is deprecated, what is the "Rails 4" way of preparing the test database?
Since your database is in memory, you need to recreate it every time. See this blog post
Add the following code right below the “# Include your application
configuration below” line in environment.rb.
def in_memory_database?
ENV["RAILS_ENV"] == "test" and
ActiveRecord::Base.connection.class == ActiveRecord::ConnectionAdapters::SQLiteAdapter and
Rails::Configuration.new.database_configuration['test']['database'] == ':memory:'
end
if in_memory_database?
puts "creating sqlite in memory database"
load "#{RAILS_ROOT}/db/schema.rb" # use db agnostic schema by default
# ActiveRecord::Migrator.up('db/migrate') # use migrations
end
The reason for adding this code at this point and not just at the end
of the environment file is that you may have further logic in
environment that actually relies on the database being present.