I have a database table with a certain field which should be impossible to update once it has been inserted to the database. How do I tell my model that it shouldn't allow updating of a certain field?
You want to use attr_readonly:
Attributes listed as readonly will be used to create a new record but update operations will ignore these fields.
class Customer < ActiveRecord::Base
attr_readonly :your_field_name
end
Here's my related solution to a similar problem - we have fields that we want a user to be able to set themselves, we don't require them on creation of the record, but we do NOT want to them be changed once they are set.
validate :forbid_changing_some_field, on: :update
def forbid_changing_some_field
return unless some_field_changed?
return if some_field_was.nil?
self.some_field = some_field_was
errors.add(:some_field, 'can not be changed!')
end
The thing that surprised me, though, was that update_attribute still works, it bypasses the validations. Not a huge deal, since updates to the record are mass assigned in practice - but I called that out in the tests to make it clear. Here's some tests for it.
describe 'forbids changing some field once set' do
let(:initial_some_field) { 'initial some field value' }
it 'defaults as nil' do
expect(record.some_field).to be nil
end
it 'can be set' do
expect {
record.update_attribute(:some_field, initial_some_field)
}.to change {
record.some_field
}.from(nil).to(initial_some_field)
end
context 'once it is set' do
before do
record.update_attribute(:some_field, initial_some_field)
end
it 'makes the record invalid if changed' do
record.some_field = 'new value'
expect(record).not_to be_valid
end
it 'does not change in mass update' do
expect {
record.update_attributes(some_field: 'new value')
}.not_to change {
record.some_field
}.from(initial_some_field)
end
it 'DOES change in update_attribute!! (skips validations' do
expect {
record.update_attribute(:some_field, 'other new value')
}.to change {
record.some_field
}.from(initial_some_field).to('other new value')
end
end
end
Related
In my model definition, I have
# models/my_model.rb
# == Schema Information
#
# Table name: my_models
#
# id :bigint not null, primary key
# another_model_id :bigint
# field_1 :string
# field_2 :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_my_models_on_another_model_id (another_model_id) UNIQUE
class MyModel < ApplicationRecord
belongs_to :another_model
def update_from_api_response(api_response)
$stderr.puts("UPDATE")
self.field_1 = api_response[:field_1]
self.field_2 = api_response[:field_2]
end
def update_my_model!(api_response)
ApplicationRecord.transaction do
$stderr.puts("HELLO")
update_from_api_response(api_response)
$stderr.puts("WORLD")
self.save!
end
end
end
I put in some puts statements to check whether my code entered the function. If everything works alright, the program should log "HELLO", "UPDATE", then "WORLD".
In my model spec I have
# spec/models/my_model_spec.rb
RSpec.describe MyModel, type: :model do
let(:my_model) { create(:my_model) }
let(:api_response) {
{
:field_1 => "field_1",
:field_2 => "field_2",
}
}
describe("update_my_model") do
it "should update db record" do
expect(my_model).to receive(:update_from_api_response)
.with(api_response)
expect(my_model).to receive(:save!)
expect{ my_model.update_my_model!(api_response) }
.to change{ my_model.field_1 }
end
end
end
The factory object for MyModel is defined like this (it literally does not do anything)
# spec/factories/my_models.rb
FactoryBot.define do
factory :my_model do
end
end
The output from the puts (this appears before the error message)
HELLO
WORLD
Interestingly, "UPDATE" is not printed, but it passes the receive test.
The change match test fails, and the output from the console is as follows
1) MyModel update_my_model should update db record
Failure/Error:
expect{ my_model.update_my_model(api_response) }
.to change{ my_model.field_1 }
expected `my_model.field_1` to have changed, but is still nil
# ./spec/models/my_model_spec.rb
# ./spec/rails_helper.rb
I suspected that it might have something to do with me wrapping the update within ApplicationRecord.transaction do but removing that does nothing as well. "UPDATE" is not printed in both cases.
I've also changed the .to receive(:update_from_api_response) to .to_not receive(:updated_from_api_response) but it throws an error saying that the function was called (but why is "UPDATE" not printed then?). Is there something wrong with the way I'm updating my functions? I'm new to Ruby so this whole self syntax and whatnot is unfamiliar and counter-intuitive. I'm not sure if I "updated" my model field correctly.
Thanks!
Link to Git repo: https://github.com/jzheng13/rails-tutorial.git
When you call expect(my_model).to receive(:update_from_api_response).with(api_response) it actually overrides the original method and does not call it.
You can call expect(my_model).to receive(:update_from_api_response).with(api_response).and_call_original if you want your original method to be called too.
Anyway, using "expect to_receive" and "and_call_original" rings some bells for me, it means you are testing two different methods in one test and the tests actually depends on implementation details instead of an input and an output. I would run two different tests: test that "update_from_api_response" changes the fields you want, and maybe test that "update_my_model!" calls "update_from_api_response" and "save!" (no need to test the field change, since that would be covered on the "update_from_api_response" test).
Thank you, the separate Github file works wonders.
This part works fine:
Put it in a separate expectation and it works fine:
describe("update_my_model") do
it "should update db record" do
# This works
expect{ my_model.update_my_model!(api_response) }.to change{ my_model.field_one }
end
end
How is it triggered?
But here is your problem:
expect(my_model).to receive(:update_from_api_response).with(api_response)
expect(my_model).to receive(:save!)
This means that you are expecting my model to have update_from_api_response to be called with the api_response parameter passed in. But what is triggering that? Of course it will fail. I am expecting my engine to start. But unless i take out my car keys, and turn on the ignition, it won't start. But if you are expecting the car engine to start without doing anything at all - then of course it will fail! Please refer to what #arieljuod has mentioned above.
Also why do you have two methods: update_from_api_response and update_my_model! which both do the same thing - you only need one?
I have a User class with a save method which makes a change to one of the user instance attributes. Specifically, each user has an options hash that gets one of its values deleted during the save process.
I have an rspec test with 2 context groups. Each group creates a new #user object using FactoryGirl.build(:user). When I call #user.save in the first context group, the attribute change occurs as expected. However, the second time that FactoryGirl.build(:user) gets called, it doesn't return a User object according to the FactoryGirl definition. It returns a user object with an options hash that is missing the same value that gets deleted during the save process. This object is not valid, and as a result #user.save fails the second time.
UPDATE: I tried changing the variable names and I still have the same problem. The issue seems to be with the FactoryGirl :user factory being modified somehow during the first example, resulting in the second example failing.
Below is a simplified version of my code. Whichever context group is executed second ("with avatar" or "without avatar") when run randomly by Rspec is the one that fails. I have used puts in both cases to confirm that the second #user has a bad options hash, and causes the test to fail.
describe "save" do
context "with avatar" do
before(:context) do
#user = FactoryGirl.build(:user)
puts #user
#save_result = #user.save
end
after(:context) do
delete_user(#user)
end
it "should return true" do
expect(#save_result).to be true
end
end
context "without avatar" do
before(:context) do
#user = FactoryGirl.build(:user, avatar: nil)
puts #user
#save_result = #user.save
end
after(:context) do
delete_user(#user)
end
it "should return true" do
expect(#save_result).to be true
end
end
end
I suspect that the options hash gets reused.
According to the FactoryGirl readme, when you want to add a hash attribute to a FactoryGirl definition and that hash is dynamic (i.e. not the same among all created instances), you need to wrap it in a block:
Instead of:
factory :user do
options { option1: 1, option2: 2 }
end
You need to do:
factory :user do
options { { option1: 1, option2: 2 } }
end
I have two models Article and ArticleVote. When I destroy an article vote (user cancels his vote), I want article's score to be changed. So I made a callback. Here is what my ArticleVote model looks like:
class ArticleVote < ActiveRecord::Base
belongs_to :article
belongs_to :user
before_destroy :before_destroy
validates :value, inclusion: {in: [1, -1]}
def self.upvote(user, article)
cast_vote(user, article, 1)
end
def self.downvote(user, article)
cast_vote(user, article, -1)
end
private
def self.cast_vote(user, article, value)
vote = ArticleVote.where(user_id: user.id, article_id: article.id).first_or_initialize
vote.value = value
vote.save!
article.score += value
article.save!
end
def before_destroy
article.score -= value
article.save
end
end
My ArticleVote#destroy test fails:
context '#destroy' do
let(:user) { FactoryGirl.create(:user) }
let(:article) { FactoryGirl.create(:article) }
it 'changes article score by negative vote value' do
ArticleVote.upvote(user, article)
expect{ ArticleVote.where(user: user, article: article).first.destroy }.to change{ article.score }.by -1
end
end
Failures:
1) ArticleVote voting #destroy should change article score by nevative vote value
Failure/Error: expect{ ArticleVote.where(user: user, article: article).first.destroy }.to change{ article.score }.by -1
result should have been changed by -1, but was changed by 0
# ./spec/models/article_vote_spec.rb:32:in `block (4 levels) in '
When I change my test to this, it passes:
context '#destroy' do
let(:user) { FactoryGirl.create(:user) }
let(:article) { FactoryGirl.create(:article) }
it 'changes article score by nevative vote value' do
ArticleVote.upvote(user, article)
vote = ArticleVote.where(user: user, article: article).first
expect{ vote.destroy }.to change{ vote.article.score }.by -1
end
end
Shouldn't these two be equivalent? Shouldn't my article and vote.article reference to same instance?
In your first test you are creating new Article object in the memory. Rails is not going to check attribute values in db every time you call article.score as it would make everything extremely slow - those value are stored in the memory (it is kind-of caching the results). Hence article.score is not going to change at any point. You need to tell rails to reload all the attributes from the database - use article.reload.score within change block.
Additional explanation:
Let say we did:
model_1 = Model.where(<condition>).first
model_2 = Model.where(<some condition>).first
Both model_1 and model_2 are created from some row in the database, however they are different objects in the memory. Hence when you do:
model_1.some_attribute = 'new value'
model_1.save
model_2.some_attribute #=> 'old_value'
The reason is performance - Rails is not going to check the database whether given attribute has changed or not within database. model_2 did the sql query when it was created and will not re-check until you tell it to do so.
However in most cases there is no point in creating two duplicate objects in the memory and it is the best practice not to do so. It is not always as obvious where those obejcts are created. In case of your first test, the problem is that ArticleVote.where(user: user, article: article).first.article is a duplicate of your original article object, hence your before_save callback follows same pattern as model_1, model_2 example.
Best way to avoid such a problems is a proper use of associations, including inverse_of option and using model.associations.where(...) in place of AssocatedClass.where(model: model, ...) or model.association.create(...) in place of 'AssociationClass.create(model: model, ...)
I have a Private methods in my model like the following:
validate :record_uniq
private
def record_uniq
if record_already_exists?
errors.add(:base, "already exists")
end
end
def record_already_exists?
question_id = measure.question_id
self.class.joins(:measure).
where(measures: {question_id: ques_id}).
where(package_id: pack_id).
exists?
end
This methods is more like a uniqueness scope thing to prevent duplicate records. I want to know how to write test for validate :record_uniq by using shoulda or rspec?
Example of what i tried:
describe Foo do
before do
#bar = Foo.new(enr_rds_measure_id: 1, enr_rds_package_id: 2)
end
subject { #bar }
it { should validate_uniqueness_of(:record_uniq) }
end
Simple - build an object that fails the validation, validate it, and verify that the correct error message has been set.
For example (if you had a model named City):
it 'validates that city is unique' do
city = City.new # add in stuff to make sure it will trip your validation
city.valid?
city.should have(1).error_on(:base) # or
city.errors(:base).should eq ["already exists"]
end
Here's what I would do using RSpec 3 syntax.
it 'validates that city is unique' do
city = City.new('already taken name')
expect(city).to be_invalid
expect(city.errors[:base]).to include('already exists')
end
My Rails models: task has_many positions.
Scenario: When I create a new position, it should create itself a task. I'd like to test that, and I'm doing it like this:
context "creating a new position" do
let(:position) { create :position, name: 'Read some books', :task => nil }
it "should create a simple task" do
Task.find_by_name('Read some books').should be_nil # First should
position # Execute let() block (FactoryGirl is lazy evaluating)
Task.find_by_name('Read some books').should_not be_nil # Second (more relevant) should
end
end
So how should I improve my test? The first "should" simply makes sure that there isn't already a Task, so we can be sure that creating the Position creates the Task. But this violates the "only one should per it block" principle. So what about this?
context "creating a new position" do
let(:position) do
position = create :position, name: 'Read some books', :task => nil
Task.delete_all
position
end
it "should create a simple task" do
position # Execute let() block (FactoryGirl is lazy evaluating)
Task.find_by_name('Read some books').should_not be_nil
end
end
Or should I simply count on the fact that there shouldn't be such a task anyways (because a clean test db wouldn't have one)? Thanks for your opinions.
Update (Solution)
After some research I found the change matcher of RSpec:
let(:position) { create :position, name: 'Read some books', :task => nil }
it "should create a simple task" do
# Thanks to FactoryGirl's lazy evaluation of let(), the position doesn't yet exist in the first place, and then after calling position in the expect{} block, it is created.
expect { position }.to change{ Task.count(conditions: { name: 'Read some books' }) }.by(1)
end
What to Test
I will not address in detail whether the tests themselves are useful to any degree. To me, they seem to be exercising basic database functions rather than application logic, which is of marginal utility, but only you can really decide what's important to test.
Be Specific
In the example you give, there's no real reason to use a let block, which memoizes the variable. If only one test needs the record, instantiate it just in that specific test. For example:
context 'creating a new position' do
it 'should be nil when the position record is missing' do
Task.find_by_name('Read some books').should be_nil
end
it 'should successfully create a position' do
create :position, name: 'Read some books', :task => nil
Task.find_by_name('Read some books').should_not be_nil
end
end
Alternatively, if you're trying to test how your application behaves when a record is missing, then go ahead and memoize a variable or create a record in a before block, but explicitly delete the record in that one specific test.
Multiple Contexts
Finally, if you're finding that you have too much state to set up in individual tests, that's usually a clue that you should consider splitting your tests into different contexts. For example, you might want to separate tests into one context that checks behavior when a record doesn't exist, and a separate context for when records do exist.
Like all things testing, it's an art more than a science. Your mileage may vary.
RSpec 2.11 allows you to pass a block to change, and it expects the return value of the block to be the thing that changes. I would expect this to work for you:
expect { position }.to change { Task.where(:name => 'Read some books').count }.from(0).to(1)