ruby-on-railsrspecacts-as-paranoid

RSpec: paranoid2 - can't modify frozen Hash


I'm working on RSpec tests for my model which uses paranoid2 gem. This is some kind of paranoia or acts_as_paranoid - overwrites .delete and .destroy and some other with it's own versions which marks data as deleted rather than deleting it until you force it to with (forced: true) parameter.

My app works just fine and I have problems only with the rspec.

spec/models/slide_spec.rb:

describe Slide do

 let (:slide) { build(:slide) }

  describe "after .destroy(force: true)" do
   before do      
      slide.save
      slide.destroy(force: false)      
   end
    it "is invisible" do
      expect{slide.destroy(force: true)}.to_not change(Slide, :count)
    end
    it "visible if .only_deleted" do
      expect{slide.destroy(force: true)}.to change(Slide.only_deleted, :count).by(-1)
    end    
    it "visible if .with_deleted" do
      expect{slide.destroy(force: true)}.to change(Slide.with_deleted, :count).by(-1)
    end
  end
 end

rspec output:

  after .destroy(force: true)
    visible if .with_deleted (FAILED - 1)
    visible if .only_deleted (FAILED - 2)
    is invisible (FAILED - 3)

Failures:

  1) Slide after .destroy(force: true) visible if .with_deleted
     Failure/Error: expect{slide.destroy(force: true)}.to change(Slide.with_deleted, :count).by(-1)
     RuntimeError:
       can't modify frozen Hash
     # ./spec/models/slide_spec.rb:52:in `block (4 levels) in <top (required)>'
     # ./spec/models/slide_spec.rb:52:in `block (3 levels) in <top (required)>'

  2) same as above

  3) same sa above

/app/model/slide.rb:

class Slide < ActiveRecord::Base
    paranoid
    ...

Solution

  • This happens because Rails marks the internal attributes hash with freeze after calling destroy on a model. This frozen hash prohibits the object from further change: A later destroy(force: true) wants to remove the id, a reload wants to override some attributes with fresh value from the database - both will fail.

    The only way to avoid this problem is to reload the object manually:

    describe "after .destroy(force: true)" do
      before do
        slide.save
        slide.destroy(force: false)
        @slide = Slide.with_deleted.find(slide.id)  # manual reload
      end
      it "is invisible" do
        expect{@slide.destroy(force: true)}.to_not change(Slide, :count)
      end
      it "visible if .only_deleted" do
        expect{@slide.destroy(force: true)}.to change(Slide.only_deleted, :count).by(-1)
      end
      it "visible if .with_deleted" do
        expect{@slide.destroy(force: true)}.to change(Slide.with_deleted, :count).by(-1)
      end
    end