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
ortest_module = Module.new(ParentModule)
.