RSpec Class Variable Testing
class Foo
DEFAULT_ITEMS = %w(foo bar baz)
class << self
attr_accessor :items
end
@items = DEFAULT_ITEMS
end
describe Foo do
before(:each) { Foo.class_variable_set :@items, Foo::DEFAULT_ITEMS }
it "should have a default" do
Foo.items.should eq(Foo::DEFAULT_ITEMS)
end
it "should allow items to be added" do
Foo.items << "kittens"
Foo.items.include?("kittens").should eq(true)
end
end
Or maybe a better way is to reload the class
describe 'items' do
before(:each) do
Object.send(:remove_const, 'Foo')
load 'foo.rb'
end
end
How can I clear class variables between rspec tests in ruby
describe DataFactory
before(:each) { DataFactory.class_variable_set :@@url, nil }
...
end
Rspec testing method that modifies instance variable
Here's a concise example
describe MyClass do
describe "#update" do
it "appends the argument to the array" do
subject.update(value = 'some_value')
expect(subject.ary).to eq([value])
end
end
end
A few notes compared to your initial code. First of all, you can rely on subject
. subject
is defined as
described_class.new
It's like having
describe MyClass do
subject { described_class.new }
describe "#update" do
...
end
end
or
describe MyClass do
describe "#update" do
subject = described_class.new
end
end
which essentially means
describe MyClass do
describe "#update" do
subject = MyClass.new
end
end
If you just want to test an instance, use subject
. Do not use before actions to set instance variables.
The following line
subject.update(value = 'some_value')
expect(subject.ary).to eq([value])
is essentially a shortcut for
value = 'some_value'
subject.update(value)
expect(subject.ary).to eq([value])
It helps me to not write the expected string twice. The rest should be quite easy to understand.
You may also want to test the return value, if this is relevant.
describe MyClass do
describe "#update" do
it "appends the argument to the array" do
subject.update(value = 'some_value')
expect(subject.ary).to eq([value])
end
it "returns the item that was added" do
value = 'some_value'
expect(subject.update(value)).to eq(value)
end
end
end
How to access instance variables in rspec tests
This is how I ended up fixing this. I think this is more along the lines of what some of my commenters were saying. This also provides a little more info on the context of pic
Test:
require 'rails_helper'
RSpec.describe TrelloToAc::Card do
let(:trello_card) do
cassette = 'trello_to_ac/cards/get'
VCR.use_cassette(cassette, match_requests_on: [:method]) do
Trello::Card.find 'itZmoIG4'
end
end
let(:email_sent_label) do
cassette = 'trello_to_ac/labels/get'
VCR.use_cassette(cassette, match_requests_on: [:method]) do
Trello::Label.find '60db99d5230c123a1ab808d0'
end
end
let(:trello_to_ac_card) do
TrelloToAc::Card.new(trello_card, 32188, 147, email_sent_label)
end
describe '#pic' do
it 'returns pic url from trello card' do
cassette = 'trello_to_ac/attachments/get'
result = VCR.use_cassette(cassette, match_requests_on: [:method]) do
trello_to_ac_card.pic
end
expect(result).to be_a String
end
end
end
Actual code:
class TrelloToAc::Card
attr_reader(
:card, :order_number, :list_ac_campaign_id, :email_sent_label
)
def initialize(card, order_number, list_ac_campaign_id, email_sent_label)
@card = card
@order_number = order_number
@list_ac_campaign_id = list_ac_campaign_id
@email_sent_label = email_sent_label
end
def send_email_if_needed
#
#
attach_trello_pic_to_ac_contact if pic
#
#
end
def pic
@pic ||= card.attachments.last.url if card.attachments.any?
end
end
This way I am testing an instance of the class, not instance variables. I have similar tests for other methods as well.
RSpec testing of a class which uses a gem object as an instance variable
In RSpec, 'receive' is a method that accepts a symbol that represents the name of a method. (It allows you to chain a 'with' method that accepts the expected list of parameters.) To fix the before-block you could do this:
before do
allow(subject.instance_variable_get(:@instance_variable1).gemObjectMethod1).to receive(:gemObjectMethod2).with("sample").and_return([])
end
The sheer ugliness of that, though, suggests that something is wrong. And it is. The code is violating the law of demeter pretty badly and the test is being drawn into it.
As a first attempt to clean it up, you might consider a method that "caches" the results of calling @instance_variable1.gemObjectMethod1. Let's say that that first method returns an enumerable group of widgets. You could change your class to include something like this:
def check_validity(a_string)
widgets.gemObjectMethod2(a_string).empty?
end
private
def widgets
@widgets ||= @instance_variable1.gemObjectMethod1
end
Your class still knows a bit too much about the gem object, but now you have broken it down in such a way that you could refactor how you find widgets -- perhaps a different gem or your own implementation of it. For the purposes of your testing, you can isolate that decision from the test by mocking widgets.
let(:gem_widgets) do
instance_double(GemObjectMethod1ResultClass, gemObjectMethod2: true)
end
before do
allow(subject).to receive(:widgets).and_return(gem_widgets)
allow(gem_widgets).to receive(:gemObjectMethod2).with("sample").
and_return([])
end
it 'should pass with "sample"' do
expect(actual).to eql true
end
Class Variables in RSpec
You should stub the role
attributes to return the need values:
it "returns child organization as user" do
allow(RootOrganization).to receive(:role).and_return("Admin")
allow(Organization).to receive(:role).and_return("User")
expect(ChildOrganizationOne.role).to eq "User"
end
How do I test an instance variable in a controller?
Your mentors are being stupid. While it can be good to understand controller specs as they are everywhere in legacy applications adding them to new applications is not a good practice.
For new Rails apps: we don't recommend adding the
rails-controller-testing gem to your application. The official
recommendation of the Rails team and the RSpec core team is to write
request specs instead. Request specs allow you to focus on a single
controller action, but unlike controller tests involve the router, the
middleware stack, and both rack requests and responses. This adds
realism to the test that you are writing, and helps avoid many of the
issues that are common in controller specs.
DHH explains pretty well what is wrong with testing the instance variables of your controller and what you should be doing instead:
Testing what instance variables are set by your controller is a bad
idea. That's grossly overstepping the boundaries of what the test
should know about. You can test what cookies are set, what HTTP code
is returned, how the view looks, or what mutations happened to the DB,
but testing the innards of the controller is just not a good idea.
Which is why assigns
was extracted to a separate gem. It all boils down to testing what your code does - not how it does it.
See:
- https://github.com/rspec/rspec-rails/issues/1838
- https://github.com/rails/rails/issues/22496
- https://github.com/rails/rails/issues/18950
Related Topics
Getting a Rogue Iteration from My .Each Loop
Inserting an Array into Every Nth Element of Another
Why Is Autoload Failing to Load Files for Gems
Ruby CSV.Each in While Loop Not Executing Second Time Through
There Is a Way to Handle 'After_Save' and 'After_Destroy' "Equally"
Assign to an Array and Replace Emerged Nil Values
Rmagick How to Convert Image in Memory
How to Fix in Ruby on Rails the Undefined Method 'Alias_Method_Chain' Error
Iterate Through Array of Hashes in Ruby
Hardcoded "Require 'Debug'" Can't Find the Sourcefile
Undefined Method Error When Creating Delayed_Job Workers with Script/Delay_Job
Integrate Shoes into Aptana Studio Radrails
How to Handle Combination []+= for Auto-Vivifying Hash in Ruby
Uploading a File to a S3 Presigned Url
Quote All Fields in CSV Output
Ternary Expression with "Defined" Returns "Expression" Instead of Value