How to stub has_many association in RSpec - ruby

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

Related

FactoryBot is not creating the associated models list in the after create callback

I have two factories as follows:
FactoryBot.define do
factory :proofread_document do
factory :proofread_document_with_paragraphs do
after(:create) {|instance| create_list(:paragraph, 5, proofread_document: instance) }
end
end
end
FactoryBot.define do
factory :paragraph do
level { 1 }
association :proofread_document
end
end
In my RSpec test:
describe '#number_of_paragraphs_for' do
let(:proofread_document) { create(:proofread_document_with_paragraphs)}
it 'returns the number of paragraphs for the given level' do
expect(proofread_document.number_of_paragraphs_for("level_1")).to eq(1)
end
end
The test fails because there are no paragraphs:
proofead_document.paragraphs
=> []
Why are the associated paragraph objects not being created?
Associations are not magically reloaded on existing instances. This is not due to FactoryBot, but to ActiveRecord itself.
# example with activerecord:
class Foo
has_many :bars
end
class Bar
belongs_to :foo
end
foo = Foo.first
foo.bars
# => []
3.times { Bar.create(foo: foo) }
foo.bars
# => []
foo.reload.bars
# => [<#Bar ...>, <#Bar ...>, <#Bar ...>]
So you just need to reload the record (or just the association)
after(:create) do |inst|
create_list(...)
inst.paragraphs.reload
# or inst.reload
end
I found the problem.
In my paragraphs model I had placed a default scope as follows:
default_scope :minimum_word_count, ->{ where(proofread_word_count: MINIMUM_LEVEL_DATA_WORD_COUNT..Float::INFINITY)}
This caused some issues as the paragraph I was saving in my tests had a word count too low for the parameters defined in this scope.
#P.Boro and #rewritten helped by getting me to re-check my models and scopes.

Rspec integral factories in test

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

Ruby/Rails playing with arrays from multilevel nested associations

I have nested models, all mongoid documents :
class Project { ... has_many :tasks ...}
class Task
...
has_many :attributions, class_name: "TaskAttribution"
belongs_to :project
class TaskAttribution
...
belongs_to :task
belongs_to :user
I want to get an array of all TaskAttribution from a given project.
I'm sure I can do it with
class Project
...
def attributions
attris = Array.new
self.tasks.each do |task|
task.attributions.each do |attri|
attris << attri
end
end
end
Is the compiler doing any improvements, or should I manually do some eager loading ?
Same thing with an extra level of nesting ? (If I want an array of every user participating to the project)
class Project
...
def participating_users
users = Array.new
self.tasks.each do |task|
task.attributions.each do |attri|
attris << attri.user
end
end
users.uniq
end
EDIT : removed other questions because they were judged too subjective/not in the spirit of StackOverflow

activerecord single table inheritance on ruby 3

I have the following classes mapped with STI:
class Employee < ActiveRecord::Base
end
class StudentEmployee < Employee
# I'd like to keep university only to StudentEmployee...
end
#Just to make this example easier to understand, not using migrations
ActiveRecord::Schema.define do
create_table :employees do |table|
table.column :name, :string
table.column :salary, :integer
table.column :university, :string # Only Students
end
end
emp = Employee.create(:name=>"Joe",:salary=>20000,:university=>"UCLA")
I'd like to prevent the setting of the university field for Employees, but allow it for StudentEmployees. I tried to use attr_protected, but it will only prevent mass setting:
class Employee < ActiveRecord::Base
attr_protected :university
end
class StudentEmployee < Employee
attr_accessible :university
end
#This time, UCLA will not be assigned here
emp = Employee.create(:name=>"Joe",:salary=>20000,:university=>"UCLA")
emp.university = "UCLA" # but this will assign university to any student...
emp.save
puts "only Students should have univesities, but this guy has one..."+emp.university.to_s
The problem here is that it will insert in the database a university for simple employees.
Another problem is that I think it would be better to say in the StudentEmployee class that university is an attribute, and not to say in the Employee that university "is not" a a visible attribute... it just goes in the inverse direction of natural abstraction.
Thanks.
I would try something like this:
class Employee < ActiveRecord::Base
validate :no_university, unless: lambda { |e| e.type === "StudentEmployee" }
def no_university
errors.add :university, "must be empty" unless university.nil?
end
end
It isn't the prettiest, but it should to work.

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