Rspec integral factories in test - ruby

I apologize if my English is not perfect but it is not my native language.
I have to test a MySql database with Rails 4.2, Rspec 3.3 and FactoryGirl 4.5.
The tests of base models are green. The problems comes when I have to test a model that contains foreign keys that can't be duplicated.
At first I have two models (dimension.rb through feature.rb and technical.rb) each having a foreign key that comes from the same model (current.rb):
#models/dimension.rb
class Dimension < ActiveRecord::Base
belongs_to :current
has_many :features
...
end
#models/feature.rb
class Feature < ActiveRecord::Base
belongs_to :dimension
has_many :bxes
...
end
#models/technical.rb
class Technical < ActiveRecord::Base
belongs_to :current
has_many :bxes
...
end
These two models are placed in the final model (bxe.rb)
#models/bxe.rb
class Bxe < ActiveRecord::Base
belongs_to :feature
belongs_to :technical
...
validates :technical_id, presence: true
validates :feature_id, presence: true
end
The Current model is:
#models/current.rb
class Current < ActiveRecord::Base
has_many :technicals
has_many :dimensions
validates :current, presence: true, uniqueness: true
validates :value, presence: true, uniqueness: true
end
The factories are the following:
#spec/factories/current.rb
FactoryGirl.define do
factory :current do
trait :lower do
current '800A'
value 800
end
trait :higher do
current '2000A'
value 2000
end
end
end
#spec/factories/dimension.rb
FactoryGirl.define do
factory :dimension do
...
trait :one do
current {create(:current, :lower)}
end
trait :two do
current {create(:current, :higher)}
end
end
end
#spec/factories/feature.rb
FactoryGirl.define do
factory :feature do
descr 'MyString'
dimension { create(:dimension, :one) }
...
end
end
#spec/factories/technical.rb
FactoryGirl.define do
factory :technical do
...
trait :A do
current { create(:current, :lower) }
end
trait :L do
current { create(:current, :higher) }
end
end
end
#spec/factories/bxes.rb
FactoryGirl.define do
factory :bxe do
...
technical {create(:technical, :A) }
feature
end
end
When I run the test on the models the first command (technical) runs and the factory creates a Current record with id = 1 but the second (features) fails, since the factory try again to create the record of Current with the same data, action prohibited from the model current.rb
#rspec spec/models
2.1.2 :001 > FactoryGirl.create(:bxebusbar, :one)
... Current Exists ... SELECT 1 AS one FROM `currents` WHERE `currents`.`current` = BINARY '800A' LIMIT 1
... INSERT INTO `currents` (`current`, `value`, `created_at`, `updated_at`) VALUES ('800A', 800, ..., ...)
... INSERT INTO `technicals` (..., `current_id`, ..., `created_at`, `updated_at`) VALUES (..., 1, ..., ...)
... Current Exists ... SELECT 1 AS one FROM `currents` WHERE `currents`.`current` = BINARY '800A' LIMIT 1
... ActiveRecord::RecordInvalid: Validation failed: Current has already been taken, Value has already been taken
I think that the problem can be solved by creating once only Current record and then using it in the technical and features factories, what would happen in reality, but I do not know how to do that.
Any suggestion? Thanks

You can use sequences to generate values that will not be duplicated. Another option is to use DatabaseCleaner and setup it to clean database after each test.
With first option:
FactoryGirl.define do
factory :current do
trait :lower do
sequence :current do { |n| "#{n}A" }
sequence :value do {|n| "#{n}" }
end
end
end
Or to setup DataBase Cleaner: Database cleaner

Related

How to stub has_many association in RSpec

I'm try to stub has_many association in RSpec because building records is so complicated. I want to detect which author has science book by using Author#has_science_tag?.
Model
class Book < ApplicationRecord
has_and_belongs_to_many :tags
belongs_to :author
end
class Tag < ApplicationRecord
has_and_belongs_to_many :books
end
class Author < ApplicationRecord
has_many :books
def has_science_tag?
tags = books.joins(:tags).pluck('tags.name')
tags.grep(/science/i).present?
end
end
RSpec
require 'rails_helper'
RSpec.describe Author, type: :model do
describe '#has_science_tag?' do
let(:author) { create(:author) }
context 'one science book' do
example 'it returns true' do
allow(author).to receive_message_chain(:books, :joins, :pluck).with(no_args).with(:tags).with('tags.name').and_return(['Science'])
expect(author.has_science_tag?).to be_truthy
end
end
end
end
In this case, using receive_message_chain is good choice? Or stubbing has_many association is bad idea?
Why dont you make use of FactoryBot associations?
FactoryBot.define do
# tag factory with a `belongs_to` association for the book
factory :tag do
name { 'test_tag' }
book
trait :science do
name { 'science' }
end
end
# book factory with a `belongs_to` association for the author
factory :book do
title { "Through the Looking Glass" }
author
factory :science_book do
title { "Some science stuff" }
after(:create) do |book, evaluator|
create(:tag, :science, book: book)
end
end
end
# author factory without associated books
factory :author do
name { "John Doe" }
# author_with_science_books will create book data after the author has
# been created
factory :author_with_science_books do
# books_count is declared as an ignored attribute and available in
# attributes on the factory, as well as the callback via the evaluator
transient do
books_count { 5 }
end
# the after(:create) yields two values; the author instance itself and
# the evaluator, which stores all values from the factory, including
# ignored attributes; `create_list`'s second argument is the number of
# records to create and we make sure the author is associated properly
# to the book
after(:create) do |author, evaluator|
create_list(:science_book, evaluator.books_count, authors: [author])
end
end
end
end
This allows you to do:
create(:author).books.count # 0
create(:author_with_science_books).books.count # 5
create(:author_with_science_books, books_count: 15).books.count # 15
So your test becomes:
RSpec.describe Author, type: :model do
describe '#has_science_tag?' do
let(:author_with_science_books) { create(:author_with_science_books, books_count: 1) }
context 'one science book' do
it 'returns true' do
expect(author_with_science_books.has_science_tag?).to eq true
end
end
end
end
And you could also refactor Author#has_science_tag?:
class Author < ApplicationRecord
has_many :books
def has_science_tag?
books.joins(:tags).where("tags.name ILIKE '%science%'").exists?
end
end

find_or_create_by on intermediate table in rails 2.3

I have 2 tables , related to a third table as follows.
class A < ActiveRecord::Base
has_many :B, :through => :AB
has_many :AB
end
class B < ActiveRecord::Base
has_many :A, :through => :AB
has_many :AB
end
class AB < ActiveRecord::Base
belongs_to :B,
belongs_to :A
end
I want to be able to insert into the intermediate table(AB), using find_or_create.But find_or_create doesnot work on AB. How can I do it, other than finding and creating seperately ??
PS: I dont want to use has_and_belongs_to_many because that doesnot create the intermediate model and i want to insert into the intermediate table without creating any extra rows in the end tables, A and B.
Some suggestions please??
find_or_create_by has been added in rails 4. You can write it by yourself (should go to initializers):
class ActiveRecord::Base
def self.find_by(params)
scoped.where(params).limit(1).first
end
def self.find_or_create_by(params)
find_by(params) || create(params)
end
end

Rails nested form on many-to-many: how to prevent duplicates?

I've setup a nested form in my rails 3.2.3 app, it's working fine, my models are:
class Recipe < ActiveRecord::Base
attr_accessible :title, :description, :excerpt, :date, :ingredient_lines_attributes
has_and_belongs_to_many :ingredient_lines
accepts_nested_attributes_for :ingredient_lines
end
and:
class IngredientLine < ActiveRecord::Base
attr_accessible :ingredient_id, :measurement_unit_id, :quantity
has_and_belongs_to_many :recipes
belongs_to :measurement_unit
belongs_to :ingredient
end
As above, a Recipe can have multiple IngredientLines and vice versa.
What I'm trying to avoid is record duplication on IngredienLine table.
For example imagine that for recipe_1 an IngredientLine with {"measurement_unit_id" => 1, "ingredient_id" => 1, "quantity" => 3.5} is associated, if for recipe_5 the IngredientLine child form is compiled by the user with the same values, I don't want a new record on IngredientLine table, but only a new association record in the join table ingredient_lines_recipes.
Note that currently I dont't have any IngredientLine controller as saving and updating IngredientLines is handled by nested form routines. Even my Recipe controller is plain and standard:
class RecipesController < ApplicationController
respond_to :html
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new(params[:recipe])
flash[:notice] = 'Recipe saved.' if #recipe.save
respond_with(#recipe)
end
def destroy
#recipe = Recipe.find(params[:id])
#recipe.destroy
respond_with(:recipes)
end
def edit
respond_with(#recipe = Recipe.find(params[:id]))
end
def update
#recipe = Recipe.find(params[:id])
flash[:notice] = 'Recipe updated.' if #recipe.update_attributes(params[:recipe])
respond_with(#recipe)
end
end
My guess is that should be enough to override the standard create behavior for IngredientLine with find_or_create, but I don't know how to achieve it.
But there's another important point to take care, imagine the edit of a child form where some IngredientLines are present, if I add another IngredientLine, which is already stored in IngredientLine table, rails of course should not write anything on IngredientLine table, but should also distinguish between child records already associated to the parent, and the new child record for which needs to create the relation, writing a new record on the join table.
Thanks!
in Recipe model redefine method
def ingredient_lines_attributes=(attributes)
self.ingredient_lines << IngredientLine.where(attributes).first_or_initialize
end
Old question but I had the same problem. Forgot to add :id to white list with rails 4 strong_parameters.
For example:
widgets_controller.rb
def widget_params
params.require(:widget).permit(:name, :foos_attributes => [:id, :name, :_destroy],)
end
widget.rb
class Widget < ActiveRecord::Base
has_many :foos, dependent: :destroy
accepts_nested_attributes_for :foos, allow_destroy: true
end
foo.rb
class Foo < ActiveRecord::Base
belongs_to :widget
end
I have run into a similar situation and found inspiration in this answer. In short, I don't worry about the duplication of nested models until save time.
Translated to your example, I added autosave_associated_records_for_ingredient_lines to Recipe. It iterates through ingredient_lines and performs a find_or_create as your intuition said. If ingredient_lines are complex, Yuri's first_or_initialize approach may be cleaner.
I believe this has the behavior you're looking for: nested models are never duplicated, but editing one causes a new record rather than updating a shared one. There is the strong possibility of orphaned ingredient_lines but if that's a serious concern you could choose to update if that model has only one recipe with an id that matches the current one.

Many-to-Many Uniqueness Constraint Test Not Working

I have a many-to-many relationship with a join table in my Rails application. I'm using the has_many :through idiom in my models. To keep things simple, lets call my first class Student, my second class Course, and the join table class Enrollment (which contains fields student_id and course_id). I want to make sure that a given Student is associated with a given Course at most once (i.e. the {student_id, course_id} tuple should be unique in the enrollment table).
So I have a migration a that enforces this uniqueness.
def change
add_index :enrollments, [:student_id, :course_id], :unique => true
end
In addition my model classes are defined as such:
class Student < ActiveRecord::Base
has_many :enrollments
has_many :courses, :through => :enrollment
end
class Course < ActiveRecord::Base
has_many :enrollments
has_many :students, :through => :enrollment
end
class Enrollment < ActiveRecord::Base
belongs_to :student
belongs_to :course
validates :student, :presence => true
validates :course, :presence => true
validates :student_id, :uniqueness => {:scope => :course_id}
end
In a rails console, I can do the following:
student = Student.first
course = Course.first
student.courses << course
#... succeeds
student.courses << course
#... appropriately fails and raises an ActiveRecord::RecordInvalid exception
In my RSpec test, I do the exact same thing and I get no exception with the following code:
#student.courses << #course
expect { #student.courses << #course }.to raise_error(ActiveRecord::RecordInvalid)
And so my test fails and reports:
expected ActiveRecord::RecordInvalid but nothing was raised
What's going on here? What could I be doing wrong? How do I fix it?
Rails uses model level validation, if you want strict checking for uniquiness you need to use database level - foreign keys for example. But in this case you need to catch exceptions from database connector.
This is strange because in my code (very similar to your) validation for unique raises exception.
There's a couple of things here that could be happening:
#courses has changed between uses.
#student has changed between uses.
By using let you'll protect these values from changing between expectations.
let(:course) { Course.first }
let(:student) { Student.first }
subject{ student.courses << course << course }
it { should raise_error(ActiveRecord::RecordInvalid) }
Or, there could just be something wrong with your code :)

Rails3: Nested model - child validates_with method results in "NameError - uninitialized constant [parent]::[child]"

Consider the following parent/child relationship where Parent is 1..n with Kids (only the relevant stuff here)...
class Parent < ActiveRecord::Base
# !EDIT! - was missing this require originally -- was the root cause!
require "Kid"
has_many :kids, :dependent => :destroy, :validate => true
accepts_nested_attributes_for :kids
validates_associated :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
# for simplicity, assume a single field: #item
validates_presence_of :item, :message => "is expected"
end
The validates_presence_of methods on the Kid model works as expected on validation failure, generating a final string of Item is expected per the custom message attribute supplied.
But if try validates_with, instead...
class Kid < ActiveRecord::Base
belongs_to :parent
validates_with TrivialValidator
end
class TrivialValidator
def validate
if record.item != "good"
record.errors[:base] << "Bad item!"
end
end
end
...Rails returns a NameError - uninitialized constant Parent::Kid error following not only an attempt to create (initial persist) user data, but also when even attempting to build the initial form. Relevant bits from the controller:
def new
#parent = Parent.new
#parent.kids.new # NameError, validates_* methods called within
end
def create
#parent = Parent.new(params[:parent])
#parent.save # NameError, validates_* methods called within
end
The error suggests that somewhere during model name (and perhaps field name?) resolution for error message construction, something has run afoul. But why would it happen for some validates_* methods and not others?
Anybody else hit a wall with this? Is there some ceremony needed here that I've left out in order to make this work, particularly regarding model names?
After a few hours away, and returning fresh -- Was missing require "Kid" in Parent class. Will edit.

Resources