update_attributes changes attributes even if validation fails - ruby-on-rails

For example, if I run test.update_attributes prop1: 'test', prop2: 'test2' when prop1 and prop2 have validations that prevent those values, test.prop1 will still be 'test' and test.prop2 will still be 'test2'. Why is this happening and how can I fix it?

According to the Rails docs for update_attributes, it's an alias of update. Its source is as follows:
# File activerecord/lib/active_record/persistence.rb, line 247
def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
assign_attributes(attributes)
save
end
end
So, it's wrapped in a DB transaction which is why the rollback happens. However, let's check out assign_attributes. According to its source:
# File activerecord/lib/active_record/attribute_assignment.rb, line 23
def assign_attributes(new_attributes)
...
_assign_attribute(k, v)
...
end
That is defined as:
# File activerecord/lib/active_record/attribute_assignment.rb, line 53
def _assign_attribute(k, v)
public_send("#{k}=", v)
rescue NoMethodError
if respond_to?("#{k}=")
raise
else
raise UnknownAttributeError.new(self, k)
end
end
So, when you call test.update_attributes prop1: 'test', prop2: 'test', it basically boils down to:
test.prop1 = 'test'
test.prop2 = 'test'
test.save
If save fails the validations, our in-memory copy of test still has the modified prop1 and prop2 values. Hence, we need to use test.reload and the issue is resolved (i.e. our DB and in-memory versions are both unchanged).
tl;dr Use test.reload after the failed update_attributes call.

Try wrapping it in an if-statement:
if test.update(test_params)
# your code here
else
# your code here
end

This is working as designed. For example the update controller method usually looks like this:
def update
#test = Test.find(params[:id])
if #test.update(test_attributes)
# redirect to index with success messsage
else
render :edit
end
private
def test_attributes
# params.require here
end
end
The render :edit will then re-display the form with an error message and the incorrect values filled in for the user to correct. So you actually do want the incorrect values to be available in the model instance.

Related

Rails5: Transaction + how to test it

I have a class with following transaction:
# frozen_string_literal: true
class InactivateEmployee
include ServiceResult
def call(id)
begin
ActiveRecord::Base.transaction do
employee = Employee.find(id)
employee.update(is_active: false)
if employee.tasks.any?
employee.tasks.delete_all
end
response(code: 204, value: employee)
rescue ActiveRecord::ActiveRecordError
raise ActiveRecord::Rollback
end
rescue ActiveRecord::Rollback => e
response(code: 422, errors: e)
end
end
end
where ServiceResult is:
# frozen_string_literal: true
# ServiceResult should be included in each Service Class to have a unified returned object from each service
ServiceResultResponse = Struct.new(:success?, :response_code, :errors, :value, keyword_init: true)
module ServiceResult
def response(code:, errors: nil, value: nil )
ServiceResultResponse.new(
success?: code.to_s[0] == '2',
response_code: code,
errors: errors,
value: value
)
end
end
Question 1:
Is this code ok? what could be improved?
Question 2
How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error? i tried sth like that - but it does not work....
before do
allow(ActiveRecord::Associations::CollectionAssociation).to receive(:delete_all).and_return(ActiveRecord::ActiveRecordError.new)
end
Question 1: Is this code ok? what could be improved?
First and foremost, call should not be determining response codes. That wields together making the call with a specific context. That's someone else's responsibility. For example, 422 seems inappropriate, the only possible errors here are not finding the Employee (404) or an internal error (500). In general if you're rescuing ActiveRecordError you could probably be rescuing something more specific.
Does this need to be an entire service object? It's not using a service. It's only acting on Employee. If it's a method of Employee it can be used on any existing Employee object.
class Employee
def deactivate!
# There's no need for the find to be inside the transaction.
transaction do
# Use update! so it will throw an exception if it fails.
update!(is_active: false)
# Don't check first, it's an extra query and a race condition.
tasks.delete_all
end
end
end
Something else is responsible for catching errors and determining response codes. Probably the controller. Generic errors like a database failure should be handled higher up, probably by a default template.
begin
employee = Employee.find(id)
employee.deactivate!
rescue ActiveRecord::RecordNotFound
render status: :not_found
end
render status: :no_content
In ServiceResult you're checking success with code.to_s[0] == '2', use math or a Range instead. The caller should not be doing that at all, but it does because you have a module returning a Struct which can't do anything for itself.
ServiceResult should a class with a success? method. It's more flexible, more obvious what's happening, and doesn't pollute the caller's namespace.
class ServiceResult
# This makes it act like a Model.
include ActiveModel::Model
# These will be accepted by `new`
# You had "errors" but it takes a single error.
attr_accessor :code, :error, :value
def success?
(200...300).include?(code)
end
end
result = ServiceResult.new(code: 204, error: e)
puts "Huzzah!" if result.success?
I question if it's needed at all. It seems to be usurping the functionality of render. Is it an artifact of InactivateEmployee trying to do too much and having to pass its interpretation of what happened around?
Question 2 How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error?
Now that you're not doing too much in a single method, it's much simpler.
describe '#deactivate!' do
context 'with an active employee' do
# I'm assuming you're using FactoryBot.
let(:employee) { create(:employee, is_active: true) }
context 'when there is an error deleting tasks' do
before do
allow(employee.tasks).to receive(:delete_all)
# Exceptions are raised, not returned.
.and_raise(ActiveRecord::ActiveRecordError)
end
# I'm assuming there's an Employee#active?
it 'remains active' do
# same as `expect(employee.active?).to be true` with better diagnostics.
expect(employee).to be_active
end
end
end
end

Puts statement in function is not executed, function test fails, but receive(:function) spec works

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?

Rspec: expect an association to receive a message

I'm in Rails 6 with Rspec 3.8.0.
I have a model A which belongs_to B. And I'm trying to write a unit test with A as subject:
expect(subject.b).to receive(:to_s)
subject.my_fn
Yet this spec always fails, saying that the instance of B did not receive the message, notwithstanding I have put binding.pry in the actual code to run and verified that a.b.to_s gets called:
class A
def my_fn
binding.pry
b.to_s
end
end
I have even tried:
expect(a).to receive(:b).and_return(b)
expect(b).to receive(:to_s)
And:
expect_any_instance_of(b.class).to receive(:to_s)
Yet all expectations for to_s fail. Why is this?
It's not shown in your code, but I have a feeling that you are calling the code before you set up your "receive" expectations. Simply put, the code execution should be like below:
it 'something' do
expect(subject.b).to receive(:to_s)
# write code here that would eventually call `a.b.to_s` (as you have said)
# i.e.
# `subject.some_method` (assuming `some_method` is your method that calls `a.b.to_s`
# don't call `subject.some_method` before the `expect` block above.
end
Also, just in case you don't know yet, make sure that it's the same object instance that you pass in to expect: expect(THE_ARG) ... receive() and the object that you are testing to be called. You can verify that they are the same if they have the same object_id:
it 'something' do
puts subject.b.object_id
# => 123456789
subject.some_method
end
# the class/method you're unit-testing:
class Foo
def some_method
# ...
puts b.object_id
# => 123456789
# ^ should also be the same
Otherwise if it's not the same object (object_id does not match), you would have to either use expect_any_instance_of (which I only use at the last resort as it is potentially dangerous as it is expecting "any instance")... or you could stub the chain a.b.to_s objects in your spec file.
If it's hard to stub the whole chain but at the same time, avoid the pitfalls of using expect_any_instance_of, there's another variant that I use which I use to balance convenience and spec-accuracy:
it 'something' do
expect_any_instance_of(subject.b.class).to receive(:to_s).once do |b|
expect(b.id).to eq(subject.b.id)
# the above just compares the `id` of the records (even if they are different objects in different memory-space)
# to demonstrate, say I do puts here:
puts b
# => #<SomeRecord:0x00005600e7a6f3b8 id:1 ...>
puts subject.b
# => #<SomeRecord:0x00005600e4f04138 id:1 ...>
puts b.id
# => 1
puts subject.b.id
# => 1
# notice that they are different objects (0x00005600e7a6f3b8 vs 0x00005600e4f04138)
# but the attribute id is the same (1 == 1)
end
subject.some_method
end
Seems that makes more sense you stub the b relation. It will looks like:
expect(a).to receive(:b).and_return(stub(:b, to_s: 'foo_bar')

How to set a property before saving to the database that is not in the form fields

In Rails 5 I can't seem to set a field without having the validation fail and return an error.
My model has:
validates_presence_of :account_id, :guid, :name
before_save :set_guid
private
def set_buid
self.guid = SecureRandom.uuid
end
When I am creating the model, it fails with the validation error saying guid cannot be blank.
def create
#user = User.new(new_user_params)
if #user.save
..
..
private
def new_user_params
params.require(:user).permit(:name)
end
2
Another issue I found is that merging fields doesn't work now either. In rails 4 I do this:
if #user.update_attributes(new_user_params.merge(location_id: #location_id)
If I #user.inspect I can see that the location_id is not set. This worked in rails 4?
How can I work around these 2 issues? Is there a bug somewhere in my code?
You have at least two options.
Set the value in the create action of your controller
Snippet:
def create
#user = User.new(new_user_params)
#user.guid = SecureRandom.uuid
if #user.save
...
end
In your model, use before_validation and add a condition before assigning a value:
Snippet:
before_validation :set_guid
def set_guid
return if self.persisted?
self.guid = SecureRandom.uuid
end
1
Use before_validation instead:
before_validation :set_guid
Check the docs.
2
Hash#merge works fine with rails ; your problem seems to be that user is not updating at all, check that all attributes in new_user_params (including location_id) ara valid entries for User.
If update_attributes fails, it will do so silently, that is, no exception will be raised. Check here for more details.
Try using the bang method instead:
if #user.update_attributes!(new_user_params.merge(location_id: #location_id))

Monkeypatch ActiveRecord::FinderMethods

I'm trying to monkey patch ActiveRecord::FinderMethods in order to use hashed ids for my models. So for example User.find(1) becomes User.find("FEW"). Sadly my overwritten method doesn't get called. Any ideas how to overwrite the find_one method?
module ActiveRecord
module FinderMethods
alias_method :orig_find_one, :find_one
def find_one(id)
if id.is_a?(String)
orig_find_one decrypt_id(id)
else
orig_find_one(id)
end
end
end
end
Here's an article that discusses how to actually do what you want by overriding the User.primary_key method like:
class User
self.primary_key = 'hashed_id'
end
Which would allow you to call User.find and pass it the "hashed_id":
http://ruby-journal.com/how-to-override-default-primary-key-id-in-rails/
So, it's possible.
That said, I would recommend against doing that, and instead using something like User.find_by_hashed_id. The only difference is that this method will return nil when a result is not found instead of throwing an ActiveRecord::RecordNotFound exception. You could throw this manually in your controller:
def show
#user = User.find_by_hashed_id(hashed_id)
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
Finally, one other note to make this easier on you -- Rails also has a method you can override in your model, to_param, to tell it what property to use when generating routes. By default, of course, it users the id, but you would probably want to use the hashed_id.
class User
def to_param
self.hashed_id
end
end
Now, in your controller, params[:id] will contain the hashed_id instead of the id.
def show
#user = User.find_by_hashed_id(params[:id])
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
I agree that you should be careful when doing this, but it is possible.
If you have a method decode_id that converts a hashed ID back to the original id, then the following will work:
In User.rb
# Extend AR find method to allow finding records by an encoded string id:
def self.find(*ids)
return super if ids.length > 1
# Note the short-circuiting || to fall-back to default behavior
find_by(id: decode_id(ids[0])) || super
end
Just make sure that decode_id returns nil if it's passed an invalid hash. This way you can find by Hashed ID and standard ID, so if you had a user with id 12345, then the following:
User.find(12345)
User.find("12345")
User.find(encode_id(12345))
Should all return the same user.

Resources