RSpec: Properly clean up your before(:context)` blocks

Posted . Visible to the public.

Using before(:context) is considered bad practice, because it's so easy to forget that you have to clean up all database changes done in the hook yourself. Usually RSpec uses transactions and just rolls back your changes after every block. This is really convenient, because each test just gets a blank slate of the database. However, this is not possible if your before block spans a whole context.
Having before(:context) blocks can lead to random failing of other tests - for example when the before(:context) creates objects in the database, that the other tests expected not to exist or that interferes with another object that is tested and behaves differently when the object exists.

But sometimes, if you have to test multiple expectations after doing a very expensive operation, it can come very handy to only do the expensive operation once in a before(:context) hook. In this case you can facilitate Database Cleaner Show archive.org snapshot to do the cleanup for you manually:

before(:context) do
  DatebaseCleaner.start
  do_expensive_test_setup
end

after(:context) do
  DatabaseCleaner.clean
end

My usecase was testing variants of Carrierwave file uploads. The file uploads were transformed to a different file format and scaled down to four variants. After writing the tests for that, the test suite was considerably slower than before. So I decided to use a before(:context) hook to do the expensive calculation only once and then do my expectations on each variant after that:

 context 'uploading a jpg file' do
    # do this only once because it is very expensive to calculcate the versions
    before(:context) do
      DatabaseCleaner.start
      described_class.enable_processing = true

      picture = create(:picture, :jpg_image)
      @uploader = picture.file
    end

    after(:context) do
      described_class.enable_processing = false
      # Cleanup all models and their automatically created associations like tags
      DatabaseCleaner.clean
    end

    versions = {
      'thumb' => [200, 133],
      'small' => [800, 532],
      'medium' => [1100, 732],
      'large' => [1400, 932],
    }

    versions.each do |version_name, dimensions|
      it "scales down #{version_name}" do
        expect(@uploader.send(version_name)).to have_dimensions(*dimensions)
      end

      it "is stored in an unambigous folder in 'public/uploads'" do
        regexp = "\/public\/uploads\/test\/picture\/file\/\\d+\/#{version_name}"
        expect(@uploader.send(version_name).path).to match Regexp.new(regexp)
      end
      it 'is saved as webp file' do
        expect(@uploader.send(version_name)).to be_format('webp')
      end
      it 'is writable only to the owner, readable for all and not executable' do
        expect(@uploader.send(version_name)).to have_permissions(0644)
      end
    end

    context 'the original file' do
      it 'is not scaled down' do
        expect(@uploader).to have_dimensions(1800, 1198)
      end

      it 'is not stored in the public folder but in storage' do
        expect(@uploader.file.path).to match %r(\/storage\/test\/picture\/file\/\d+\/)
      end
    end

    it "makes the image writable only to the owner, readable for all and not executable" do
      expect(@uploader).to have_permissions(0644)
    end

    it "has the correct format" do
      expect(@uploader).to be_format('JPEG')
    end
  end
Judith Roth
Last edit
Judith Roth
Posted by Judith Roth to Judith's Dev Notes (2022-06-08 05:49)