How to use rspec to test a transaction block - ruby-on-rails

I am wrapping some code in a transaction block i.e.
Tip.transaction do
...make changes to lots of tips...
end
The reason I am doing this is I want to make sure all the changes are made before being committed to the database. How do I use rspec to test that if a failure occurs before the transaction is completed, then the database will rollback to its previous state?

You can just check that no Tip was persisted to the in case of failure. The only doubt here is what does a failing tip means for you, since that's what you will reproduce in your test. A plain vanilla example:
# app/models/tip.rb
before_save :fail_if_bad_amoun
def fail_if_bad_amount
fail BadAmount if amount == 'bad'
end
def self.process(tips)
Tip.transaction do
tips.all? &:save
end
end
# spec/models/tip_spec.rb
it "does not commit in case of failure in one of the tips" do
good_tip = Tip.new(amount: 1_000)
bad_tip = Tip.new(amount: 'bad')
expect do
Tip.process([good_tip, bad_tip])
end.not_to change { Tip.count }
end

Related

RSpec: how to test database fails when there's more database operations

I have a method similar to this:
def create
reservation = Reservation.create(params[:reservation_params])
if reservation.valid?
reserved_hour = ReservedHour.create(params[:reserved_hour_params])
if reserved_hour.valid?
notification = Notification.create(params[:notification])
if !notification.valid?
reservation.destroy
reserved_hour.destroy
end
else
reservation.destroy
end
end
end
Now I'd like to test database fail cases with RSpec. For example I'd like to simulate database crash during notification creating and test if reservation and reserved_hour destroy successfully. Is there some way to do this without expanding my create method for test purposes only? I can simulate crash for all three cases by running ActiveRecord::Base.remove_connection, but I have no idea how could I test the case with a single crash.
Your code isn't going to work because all of your .create calls will always return something (either a saved record or an unsaved record) and your if statements will always be true.
Why not use .create! (which will raise an error if create is unsuccessful) within a transaction. Something like:
def create
ActiveRecord::Base.transaction do
begin
Reservation.create!(params[:reservation_params])
ReservedHour.create!(params[:reserved_hour_params])
Notification.create!(params[:notification])
rescue SomeError =>
# do something with SomeError
end
end
end
That way, your transactions will be rolled back if you have an error and you don't have to do all that .destroy business.

Triggering a method after transaction commits

ActiveRecord::Base.transaction do
do_this
Something.after_commit.action
do_that
end
# Something.action is fired/run in case no exceptions in transaction
How can one achieve this?
NOTE: one doesn't see where the transaction starts and ends (I mean transaction do and end)
As I understand your question you want to define code that should be run after the transaction inside a transation? But you do not want to run it straight away?
If this is the case it can be achieved with a proc. Info about procs.
So the code would be something like:
no_exceptions = true
ActiveRecord::Base.transaction do
do_this
after_code = Proc.new do |no_e|
# Code defining how to act no_e gives you info if exceptions where fired.
end
do_that
rescue Exception => e
no_exceptions = false
end
after_code.call(no_exceptions)
I hope this was what you where looking for.
You must use this pattern, with the save! (that raises exception on error):
begin
do_this_before_update_the_db
ActiveRecord::Base.transaction do
update_1_db_if_no_fails
update_2_db_if_no_fails
...
update_n_db_if_no_fails
end
DO-THIS-AFTER-COMMIT-ONLY-IF-NO-EXCEPTIONS
rescue Exception => exception
do_this_if_some_update_fails # depending on the exception you must raise again from here.
ensure
do_this_always_fail_or_no # this runs after commit
end
You can't put after_commit inside the transaction, because this run inside the transaction, before the commit is done.
All that is inside the transaction block runs with one commit at the end of the block. All callbacks runs inside this transaction. And all indented transactions runs inside this transaction (with only one commit). You can found detailed information here.
You must see that at the log, with only one commit at the end.
Also you can have this pattern on update_2_db_if_no_fails, with an ensure block that runs always fail or no the update_2. This run inside the first transaction, but is something different of your model after_commit callback.

atomic transaction not working - ruby rails

So I am trying to achieve atomicitiy in while doing my saves (which are essentially updates on each rows).
params[:player_types].each do |p_type_params|
if p_type_params[:id]
player = #player_types.find(p_type_params[:id])
player.assign_attributes(p_type_params)
#player_types << player
end
end
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
errors = true
end
end
end
Inspite of saving withing the transaction block, I can see partial saves also i.e. one of the rows get updated and the erroneous one does not (obviously) where as I would have wanted the updated row to be rolled back since there was atleast one row that could not be updated due to an error. Is my interpretation of transaction block correct in this case? How do I achieve an atomic save in my case?
EDIT: The model validates for uniqueness of one of the columns, which would be the reason for failing to update in the Database at this point.
You need to raise an error inside your transaction block to abort the transaction; setting errors doesn't impact the transaction.
For instance:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
if player_type.save
"DO Something.."
else
"DO something else.."
raise "save failed!"
end
end
end
The more conventional way of doing this, of course, is to use save! which raises an exception for you when it fails:
ActiveRecord::Base.transaction do
#player_types.each do |player_type|
player_type.save!
end
end
If you really need to "DO Something" on failure (besides aborting the transaction), you'll have to use the first method.

rails db transaction not rolling back if one of the db updates fails

I have the following method in my model:
class Task < ActiveRecord::Base
def update_completed_task(task_params, completed_material_params)
puts 'in update_completed_task method'
transaction do
begin
puts 'inside transaction'
self.task_finished
puts 'after task finished'
self.update_attributes!(task_params)
puts 'after update_attributes'
if completed_material_params
completed_material_params.each do |key, value|
#completed_material = CompletedMaterial.where("identity = ?", value).first
#completed_material.task = self
#completed_material.save
end
end
puts 'affter loop'
UserNotification.freelancer_has_submitted_documents(self.schedule.project, self)
puts 'after user notification change'
rescue
puts 'in rescue again yolo gandi rollback'
end
end
end
end
I am new to transactions in rails, but my understanding was that if one of the database interactions failed, the whole transaction would be rolled back. In the code above, the line:
self.update_attributes(task_params)
is failing, so the database update that occurs from self.task_finished on the line before should be rolled back. For some reason, it is not rolled back.
For background information, although i don't think it should make a difference, the "self.task_finished" line uses the state_machine gem to change the state of task. It should still rollback though. What is wrong with my transaction
You're misunderstanding what constitutes "failure"; record validations aren't "failure", they're a normal part of using your app, and they don't force any kind of database rollback.
You need to either explicitly cancel the transaction, or leave the transaction block via an exception, for the transaction to fail. Currently, you're successfully reaching the end of the transaction, so everything is happily committed to the database.
As suggested already, the best solution is update_attributes! which makes your validations into real failure by throwing an exception. As for your problem with update_attributes!...
tried that and it did do the rollback, but it also stops the program from running and the error messages don't display
That's the point. If your update_attributes! fails, there's no reason to proceed with the rest of your code, as all that does is create/update new records. The point of the transactions is to roll those changes back anyways, so the exception is doing its job perfectly by preventing that code from running.
If your validation errors aren't displaying, you should handle the exception and render normally to prevent Rails from rendering an error page when the exception leaves your method.

What's a nice way to verify within a unit test that an ActiveRecord transaction is being used?

I have a class which performs several database operations, and I want to write a unit test which verifies that these operations are all performed within a transaction. What's a nice clean way to do that?
Here's some sample code illustrating the class I'm testing:
class StructureUpdater
def initialize(structure)
#structure = structure
end
def update_structure
SeAccount.transaction do
delete_existing_statistics
delete_existing_structure
add_campaigns
# ... etc
end
end
private
def delete_existing_statistics
# ...
end
def delete_existing_structure
# ...
end
def add_campaigns
# ...
end
end
Rspec lets you assert that data has changed in the scope of a particular block.
it "should delete existing statistics" do
lambda do
#structure_updater.update_structure
end.should change(SeAccount, :count).by(3)
end
...or some such depending on what your schema looks like, etc. Not sure what exactly is going on in delete_existing_statistics so modify the change clause accordingly.
EDIT: Didn't understand the question at first, my apologies. You could try asserting the following to make sure these calls occur in a given order (again, using RSpec):
EDIT: You can't assert an expectation against a transaction in a test that has expectations for calls within that transaction. The closest I could come up with off the cuff was:
describe StructureUpdater do
before(:each) do
#structure_updater = StructureUpdater.new(Structure.new)
end
it "should update the model within a Transaction" do
SeAccount.should_receive(:transaction)
#structure_updater.update_structure
end
it "should do these other things" do
#structure_updater.should_receive(:delete_existing_statistics).ordered
#structure_updater.should_receive(:delete_existing_structure).ordered
#structure_updater.should_receive(:add_campaigns).ordered
#structure_updater.update_structure
end
end
ONE MORE TRY: Another minor hack would be to force one of the later method calls in the transaction block to raise, and assert that nothing has changed in the DB. For instance, assuming Statistic is a model, and delete_existing_statistics would change the count of Statistic in the DB, you could know that call occurred in a transaction if an exception thrown later in the transaction rolled back that change. Something like:
it "should happen in a transaction" do
#structure_updater.stub!(:add_campaigns).and_raise
lambda {#structure_updater.update_structure}.should_not change(Statistic, :count)
end

Resources