RSpec: How to define classes for specs

RSpec allows defining methods inside describe/context blocks which will only exist inside them.
However, classes (or any constants, for that matter) will not be affected by this. If you define them in your specs, they will exist globally. This is because of how RSpec works (short story: instance_eval).

describe Notifier do
  class TestRecord
    # ...
  end
  
  let(:record) { TestRecord.new }
  
  it { ... }
end

# TestRecord will exist here, outside of the spec!

This will come bite you at least when you try to define a class with the same name in another spec. Generally speaking, you don't want to pollute the global namespace in the first place.

Below you will find three examples how you can avoid polluting the global namespace. You can also read the Rubocop docs on LeakyConstantDeclaration Show archive.org snapshot about this topic.

1. Defining the constant on the example class

describe Notifier do
  class self::TestRecord < ApplicationRecord
    # ...
  end
  
  it do
    expect(self.class::TestRecord.new).to be_a(ApplicationRecord)
  end
end

Inside any let or it block, self will be the example's instance, so self.class points to the example class where TestRecord was defined. Basic Ruby.

2. Defining the class and assigning to a constant

describe Notifier do
  before do
    test_record = Class.new(ApplicationRecord) do
      # ...
    end

    stub_const('TestRecord', test_record)
  end
  
 
  it do
   expect(TestRecord.new).to be_a(ApplicationRecord)
  end
end

3. Defining the class and assigning to a variable

describe Notifier do
  let(:test_record_class) do
    Class.new(ApplicationRecord) do
       # ...
    end
  end

  it do
   expect(test_record_class.new).to be_a(ApplicationRecord)
  end
end

Each approach also works for Ruby modules, e.g. module self::TestModule < ParentModule; end or test_module = Module.new(ParentModule).

Arne Hartherz Over 6 years ago