RSpec: How to define classes for specs

Updated . Posted . Visible to the public. Repeats.

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).

Negative example:

describe Notifier do
  class TestRecord < ApplicationRecord # DO NOT do this!
    # ...
  end
  
  let(:record) { TestRecord.new }
  
  it { ... }
end

# TestRecord will exist here, outside of the spec!

Do not do this. It will bite you eventually. For example, when you try to define a class with the same name in another spec.
Generally speaking, you don't want tests to pollute the global namespace in the first place.

Below are 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.

When would I need this?

Most specs don't require you to define a class. Usually, you test the behavior of an existing class from your application.

A common use case ist testing modules which are included elsewhere. Your module may behave differently depending on the including class, be parameterized (like with Modularity Show archive.org snapshot ), or just offer an API (e.g. has_defaults) that you want to test.
While you could test that by using a domain-logic class that currently includes your module, said class may evolve at some point and could brake your tests.
You're much better off testing your module in isolation. For that, you want to define a class that includes your module, and no longer exists after your test has finished.

1. Defining the class and assigning to a constant (preferred)

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

This is usually preferred.
While the setup steps may appear unusual, your tests can just reference the class through its constant, like any other tests would use classes from your application.

2. Defining the class and assigning to a variable (also has its cases)

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

Setup is a bit shorter with this one, but your specs look a bit odd.
This can be a useful approach when the class is constructed elsewhere and/or dynamically and can not have a constant name.

3. Defining the constant on the example class (rarely necessary)

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.
You can safely define TestRecord in its context. Usually, one of the other options fits better.

Note

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

Arne Hartherz
Last edit
Arne Hartherz
Keywords
local, locally, bleed
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2017-09-06 09:54)