RSpec 'specify' passes with explicit subject, but not with implicit subject - ruby

I thought I understood how implicit subjects work in RSpec, but I don't.
Why is it that in the following example, the first spec with an explicit subject passes, but the second spec using an implicit subject fails with "undefined method `matches' for #":
class Example
def matches(str) ; true ; end
end
describe Example do
subject { Example.new }
specify { subject.matches('bar').should be_true }
it { matches('bar').should be_true }
end
(I'm using rspec 1.3, but I verified the same behavior with 2.10.1.)

Step back to some basic ruby: You're basically calling self.matches, and self in this case is an RSpec example.
You can call things like "should" on this example, with parameters, so you might try something like:
it { should matches('bar') }
but this will fail; there's no method matches on self still!
In this case, though, the subject really is the matches method, not the Example instance. So, if you want to continue using the implicit subject, your tests might be something like:
class Example
def matches(str) ; str == "bar" ; end
end
describe Example do
describe "#matches" do
let(:method) { Example.new.method(:matches) }
context "when passed a valid value" do
subject { method.call("bar") }
it { should be_true }
end
context "when passed an invalid value" do
subject { method.call("foo") }
it { should be_false }
end
end
end

I don't think you can call any methods of implicit subject. Implicit subject meanings you don't need to specify the subject, but if you want call any method you need to specify the subject.

Although Chris provided very nice answer, I recommend you to take a look at this blog post: http://blog.davidchelimsky.net/2012/05/13/spec-smell-explicit-use-of-subject/

Related

How do you test custom Bugsnag meta_data in Ruby?

How do you test custom Bugsnag meta_data (in Ruby, with Rspec)?
The code that I want to test:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = { my_extra_data: "useful info" }
end
end
The test I want to write:
context "when there's an error" do
it "calls Bugsnag with my special metadata" do
expect(Bugsnag).to receive(:notify) # TODO test meta_data values contain "my useful info"
expect do
do_something() # exception is thrown and rescued and sent to Bugsnag
end.not_to raise_error
end
end
I am using:
Ruby 2.6.6
Rspec 3.9.0
Bugsnag 6.17.0 https://rubygems.org/gems/bugsnag
The data inside of the meta_data variable is considerably more complicated than in this tiny example, which is why I want to test it. In a beautiful world, I would extract that logic to a helper and test the helper, but right now it is urgent and useful to test in situ.
I've been looking at the inside of the Bugsnag gem to figure this out (plus some Rspec-fu to capture various internal state and returned data) but at some point it's a good idea to ask the internet.
Since the metadata is complicated, I'd suggest simplifying it:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = error_metadata(e, self, 'foo')
end
end
# I assumed that you'd like to pass exception and all the context
def error_metadata(e, object, *rest)
BugsnagMetadataComposer.new(e, object, *rest).metadata
end
So now you can have a separate test for BugsnagMetadataComposer where you have full control (without mocking) over how you initialize it, and test for metadata output.
Now you only have to test that BugsnagMetadataComposer is instantiated with the objects you want, metadata is called and it returns dummy hash:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
And the hard part, ensure that metadata is assigned. To do that you can cheat a little and see how Bugsnag gem is doing it
Apparently there's something called breadcrumbs:
let(:breadcrumbs) { Bugsnag.configuration.breadcrumbs }
Which I guess has all the Bugsnag requests, last one on top, so you can do something similar to https://github.com/bugsnag/bugsnag-ruby/blob/f9c539670c448f7f129a3f8be7d412e2e824a357/spec/bugsnag_spec.rb#L36-L40
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end
And for clarity, the whole spec would look a bit like this:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end

How can I check if my subject raises an exception?

I'm currently creating an object in subject and need to test if this raises an exception. The following code illustrates what I'm trying to achieve:
describe MyClass do
describe '#initialize' do
subject { MyClass.new }
it { is_expected.not_to raise_error(Some::Error) }
end
end
I have a feeling I'm going about this the wrong way. What is the preferred way to set the subject to a new object, without creating the object twice?
Update
My problem was two-fold. Firstly, this syntax does not work:
it { is_expected.not_to raise_error }
Using expect inside an it block does, however (as pointed out by Jimmy Cuadra):
it 'does not raise an error' do
expect { subject }.not_to raise_error
end
I am not well enough acquainted with RSpec to tell you why this is.
Secondly, since RSpec 3.0.0.beta1, it is longer possible to use raise_error with a specific error class. The following, therefore, is invalid:
expect { subject }.to raise_error(Some::Error)
For more information, see
Rspec 3.0.0.beta1 changelog
Consider deprecating `expect { }.not_to raise_error(SpecificErrorClass)` #231
Remove expect {}.not_to raise_error(SomeSpecificClass) #294
If I'm understanding correctly, you're trying to test if instantiating a class causes an exception. You would just do this:
describe MyClass do
it "doesn't raise an exception when instantiated" do
expect { subject }.not_to raise_error
end
end
The right way to do that through is_expected syntax is to wrap your subject value by a Proc, like the following example:
describe MyClass do
describe '#initialize' do
subject { -> { MyClass.new } }
it { is_expected.not_to raise_error(Some::Error) }
end
end
This way is more accurate, because sometimes your use case is to expect that specific kinds of exceptions should not be thrown (while others are allowed to be thrown). This approach will cover such use cases.

Rspec testing results of instance_eval

So I'm writing a method which will result in adding some code to a class (Taking a hash of options and creating a arel-style default_scope, specifically). While I've played around with simply doing MyClass.send to get the code to the class, I feel an instance_eval will read cleaner and be more clear.
# test
let(:hash) { {order: "my_column desc"} }
let(:arel) { Proc.new{ order(hash[:order]) } }
it "converts options hash to arel calls" do
MyClass.send(:my_translator_method, hash)
end
# method
def self.my_translator_method(hash)
code = method_to_convert_options_to_arel
self.instance_eval <<CODE
default_scope #{code}
CODE
end
The problem is I'm not sure how to hook into the class in order to determine that it was changed in the way expected.
First, why can't you do something like :
self.send(:default_scope, code)
instead of using metaprogramming ?
Second, you should always test behavior, not implementation :
correct_ordered_elements = [.....]
Myclass.all.should == correct_ordered_elements
I would suggest something like this:
it "converts options hash to arel calls" do
expect { MyClass.default_scope }.to raise_error
MyClass.my_translator_method hash
expect { MyClass.default_scope }.to_not raise_error
end
and then, for good measure - check that the scope is correct also:
it "creates an ordered scope" do
MyClass.my_translator_method hash
expect(MyClass.default_scope.to_a).to eq MyClass.order("my_column desc").to_a
end
You should be able to do this with an expectation on the class singleton:
let(:code) { method_to_convert_options_to_arel }
let(:hash) { {order: "my_column desc"} }
it "sets the default scope" do
expect(MyClass).to receive(:default_scope).with(code)
MyClass.my_translator_method(hash)
end
I'd argue that you shouldn't test the scope itself here--write a separate test for the method that produces the Arel code.

rails rspec - how to check for a model constant?

How can I do something like:
it { should have_constant(:FIXED_LIST) }
In my model (active record) I have FIXED_LIST = 'A String'
It's not a db attribute or a method and I haven't been able to use responds_to or has_attribute to test for it (they fail). What can I use the to check for it. - btw I have the shoulda-matchers installed.
Based on David Chelimsky's answer I've got this to work by slightly modifying his code.
In a file spec/support/utilities.rb (or some other in spec/support) you can put:
RSpec::Matchers.define :have_constant do |const|
match do |owner|
owner.const_defined?(const)
end
end
Note the use of "RSpec::Matchers.define" in stead of "matchers"
This allows to test for constants in your specs, like:
it "should have a fixed list constant" do
YourModel.should have_constant(:FIXED_LIST)
end
Note the use of "have_constant" in stead of "have_const"
It reads a little silly, but:
describe MyClass do
it { should be_const_defined(:VERSION) }
end
The reason is that Rspec has "magic" matchers for methods starting with be_ and have_. For example, it { should have_green_pants } would assert that the has_green_pants? method on the subject returns true.
In the same fashion, an example such as it { should be_happy } would assert that the happy? method on the subject returns true.
So, the example it { should be_const_defined(:VERSION) } asserts that const_defined?(:VERSION) returns true.
If you want to say have_constant you can define a custom matcher for it:
matcher :have_constant do |const|
match do |owner|
owner.const_defined?(const)
end
end
MyClass.should have_const(:CONST)
If you're trying to use the one-liner syntax, you'll need to make sure the subject is a class (not an instance) or check for it in the matcher:
matcher :have_constant do |const|
match do |owner|
(owner.is_a?(Class) ? owner : owner.class).const_defined?(const)
end
end
See http://rubydoc.info/gems/rspec-expectations/RSpec/Matchers for more info on custom matchers.
HTH,
David
Another option to simply make sure the constant is defined – not worrying about what it's defined with:
it 'has a WHATEVER constant' do
expect(SomeClass::WHATEVER).not_to be_nil
end
A warning to anyone trying to test that constants are defined: If your code references an undefined constant while defining a class, then your specs will crash before they get to your test.
This can lead you to believe that
expect { FOO }.to_not raise_error
is failing to catch the NameError, because you'll get a big stack trace, instead of a nice "expected not to raise error, but raised NameError."
Amidst the huge stack trace, it can be difficult to notice that your test is actually crashing on line 1: requre "spec/spec_helper" because your entire application is failing to load before it gets to your actual test.
This can happen if you have dynamically defined constants, such as is done by ActiveHash::Enum, and you then use them in the definition of another constant. Don't bother testing that they exist, every spec in your app will crash if one of them fails to be defined.
You could use
defined? YOUR_MODEL::FIXED_LIST
In RSpec 2, I was able to get this to work in one line as follows:
it { subject.class.should be_const_defined(:MY_CONST) }
That is, check against the class, instead of the instance.
In My model
class Role < ActiveRecord::Base
ROLE_ADMIN = "Administrador"
end
In My rspec
RSpec.describe Role, type: :model do
let(:fake_class) { Class.new }
describe "set constants" do
before { stub_const("#{described_class}", fake_class) }
it { expect(described_class::ROLE_ADMIN).to eq("Administrador") }
end
end
For ruby 2.1.5 and rspec 3.5.0 I am able to test that constant SEARCH_CHARS_TO_IGNORE is defined in the class DiffAlertsDatatable as follows:
expect(DiffAlertsDatatable.const_defined?(:SEARCH_CHARS_TO_IGNORE)).to eq(true)

Is it possible to access the subject of the surrounding context in Rspec?

The following code doesn't work, but it best show what I'm trying to achieve
context "this context describes the class" do
subject do
# described class is actually a module here
c = Class.new.extend(described_class)
c.some_method_that_has_been_added_through_extension
c
end
# ... testing the class itself here ...
context "instances of this class" do
subject do
# this doesn't work because it introduces a endless recursion bug
# which makes perfectly sense
subject.new
end
end
end
I also tried to use a local variable in the inner context that I initialized
with the subject, but no luck. Is there any way I can access the subject of a outer scope from within my subject definition in the inner scope?
Using #subject can sometimes cause trouble. It is "primarily intended" for use with the short-hand checks like #its.
It also can make example harder to read, as it can work to mask the name/intent of what you testing. Here's a blog post that David Chelimsky wrote on the topic of #subject and #let and their role in revealing intention: http://blog.davidchelimsky.net/blog/2012/05/13/spec-smell-explicit-use-of-subject/
Try using let, instead
https://www.relishapp.com/rspec/rspec-core/v/2-10/docs/helper-methods/let-and-let
Here is how I would most likely write it.
context "this context describes the class" do
let(:name_of_the_module) { Class.new.extend(described_class) }
before do
c.some_method_that_has_been_added_through_extension
end
# ... testing the class itself here ...
context "instances of this class" do
let(:better_name_that_describes_the_instance) { klass.new }
# ... test the instance
end
end
SIDENOTE
You might want to revisit whether you want to use subject at all. I prefer using #let in almost all cases. YMMV
Something that obviously works is using an instance variable in the inner context and initializing it not with the subject but subject.call instead. Subjects are Procs. Hence, my first approach didn't work.
context "instances of this class" do
klass = subject.call
subject { klass.new }
end
I have been looking for a solution to this, but for different reasons. When I test a method that could return a value or raise an error, I often have to repeat the subject in two contexts, once as a proc for raise_error and once normally.
What I discovered is that you can give subjects names, like lets. This let's you reference an named subject from an outer scope within a new subject. Here's an example:
describe 'do_some_math' do
let!(:calculator) { create(:calculator) }
# proc to be used with raise_error
subject(:do_some_math) {
-> { calculator.do_some_math(with, complicated, args) }
}
context 'when something breaks' do
it { is_expected.to raise_error } # ok
end
context 'when everything works' do
# here we call the named subject from the outer scope:
subject { do_some_math.call } # nice and DRY
it { is_expected.to be_a(Numeric) } # also ok!
end
end

Resources