How can I clear class variables between rspec tests in ruby - ruby

I have the following class:
I want to ensure the class url is only set once for all instances.
class DataFactory
##url = nil
def initialize()
begin
if ##url.nil?
Rails.logger.debug "Setting url"
##url = MY_CONFIG["my value"]
end
rescue Exception
raise DataFactoryError, "Error!"
end
end
end
I have two tests:
it "should log a message" do
APP_CONFIG = {"my value" => "test"}
Rails.stub(:logger).and_return(logger_mock)
logger_mock.should_receive(:debug).with "Setting url"
t = DataFactory.new
t = nil
end
it "should throw an exception" do
APP_CONFIG = nil
expect {
DataFactory.new
}.to raise_error(DataFactoryError, /Error!/)
end
The problem is the second test never throws an exception as the ##url class variable is still set from the first test when the second test runs.
Even though I have se the instance to nil at the end of the first test garbage collection has not cleared the memory before the second test runs:
Any ideas would be great!
I did hear you could possibly use Class.new but I am not sure how to go about this.

describe DataFactory
before(:each) { DataFactory.class_variable_set :##url, nil }
...
end

Here is an alternative to the accepted answer, which while wouldn't solve your particular example, I'm hoping it might help a few people with a question in the same vein. If the class in question doesn't specify a default value, and remains undefined until set, this seems to work:
describe DataFactory
before(:each) do
DataFactory.remove_class_variable :##url if DataFactory.class_variable_defined? :##url
end
...
end
Works for me with a class with something more like:
def initialize
##url ||= MY_CONFIG["my value"]
...
end

Related

How to test if a DelayedJob is calling a method from another class?

I'm setting up specs to test whether or not a job calls a method under certain conditions.
This is what I have so far:
describe RandomJob do
context "when payload[:type] = MyModel" do
let!(:my_model) { create :my_model }
let!(:payload) { { type: "MyModel", id: my_model.id } }
context "when Model exists" do
it "calls MyModel.fire! with payload" do
RandomJob.perform_now(payload)
expect_any_instance_of(MyModel).to receive(:fire!).with(payload)
end
end
context "when Model does not exist" do
it "does not call MyModel.fire!" do
RandomJob.perform_now(payload)
expect_any_instance_of(MyModel).not_to receive(:fire!)
end
end
end
end
Just to be sure my way of testing worked. I setup my job like this:
class RandomJob < ApplicationJob
def perform(payload)
#payload = payload
fire_model!
end
private
def fire_model!
my_model&.fire! #payload
end
def my_model
MyModel.find(#payload[:id])
end
end
I expected the first test to pass, and the second to fail. However, my first test is failing while the second is passing.
What am I doing wrong?
You have to put the expectation before the perform_now call.
context "when Model exists" do
it "calls MyModel.fire! with payload" do
expect_any_instance_of(MyModel).to receive(:fire!).with(payload)
RandomJob.perform_now(payload)
end
end
context "when Model does not exist" do
it "does not call MyModel.fire!" do
expect_any_instance_of(MyModel).not_to receive(:fire!)
RandomJob.perform_now(payload)
end
end

How to use check something before an error is raised

I have the following ruby code
class Gateway
...
def post
begin
...
raise ClientError if state == :open
rescue ClientError => e
Log.add("error")
raise
end
end
end
On RSpec, how can I check that when ClientError is raised Log.add is called?
I have tried different things but I always get the error raised.
Thanks
You can probably do something like this (the initialize step might need to look bit different, depending on how you need to set the state to :open):
describe 'Gateway#post' do
let(:gateway) { Gateway.new(state: :open) }
before { allow(Log).to receive(:add) }
it 'raises an excpetion' do
expect { gateway.post }.to raise_error(ClientError)
expect(Log).to have_received(:add).with('error')
end
end
Something like this should work:
describe '#post' do
context 'with state :open' do
let(:gateway) { Gateway.new(state: :open) }
it 'logs the error' do
expect(Log).to receive(:add).with('error')
gateway.post rescue nil
end
it 're-raises the error' do
expect { gateway.post }.to raise_error(ClientError)
end
end
end
In the first example, rescue nil ensures that your spec is not failing because of the raised error (it silently rescues it). The second example checks that the error is being re-raised.

Testing with Rspec - The correct way

My weakest point when it comes to coding, is using TDD & BDD methods - I tend to just write code.. but it is something that I am trying to work on.
Could anyone point out the best way to go about the following problem:
Class1:
module TempMod
class MyClass
def initalize(config)
#config = config
end
def process(xml)
if react_upon? xml.something
puts 'yeah'
else
puts 'nah'
end
end
def react_upon?(xml_code)
#code here
end
end
end
So lets say I wanted to test this class, or build it from a TDD point of view so I write my tests:
describe TempMod::MyClass do
let(:config) {double}
let(:myclass) {TempMod::MyClass.new config}
context 'Given that the xml is something we react upon' do
it 'should check that it is valid' do
myclass.process '<some><xml>here</xml></some>'
end
it 'should output yea'
end
end
How do I test that it is calling the react_upon? method. Do I even want to see it is calling it?
Is the proper way to test it, to test all the functions like the react_upon? itself independently of the other functions?
This is properly the main thing that is most confusing me with this sort of testing. Am I testing the whole class, or just individually testing the functions, and not their interactions with the other functions in that class?
Also I realize the the react_upon? might not adhere to the Single responsibility principle and I would probably move that out to its own module/class which I could test using a stub.
If anyone can shed some light on this for me that would be awesome.
edit:
describe TempMod::MyClass do
let (:valid_planning_status_xml) {
'<StatusUpdate> <TitleId>2329</TitleId> <FromStatus>Proposed</FromStatus> <ToStatus>Confirmed</ToStatus> </StatusUpdate>'
}
let(:config) { double }
let(:status_resolver) { double }
subject(:message_processor) { TempMod::MyClass.new config, status_resolver }
context 'Given that the message XML is valid' do
it 'should check the context of the message' do
expect(message_processor.process valid_planning_status_xml).to call :check_me
end
context 'Given that the message is for a planning event update' do
it 'should call something' do
pending
end
end
context 'Given that the message is for a recording job update' do
end
context 'Given that the message is for a video title update' do
end
end
end
Your question confused me a bit is this what you are asking
module TempMod
class MyClass
def initalize(config)
#config = config
end
def process(xml)
react_upon?(xml.something) ? 'yeah' : 'nah'
end
def react_upon?(xml_code)
#code here
end
end
end
Then test like
describe TempMod::MyClass do
let(:config) {double}
let(:myclass) {TempMod::MyClass.new config}
context 'Given that the xml is something we react upon' do
it "should respond to react_upon?" do
expect(myclass).to respond_to(:react_upon?)
end
it "should react_upon? valid xml" do
expect(myclass.react_upon?(YOUR VALID REACTION GOES HERE)).to be_true
end
it "should not react_upon? invalid xml" do
expect(myclass.react_upon?(YOUR INVALID REACTION GOES HERE)).to be_false
end
it "should say 'yeah' if it is valid" do
expect(myclass.process('<some><xml>here</xml></some>')).to eq('yeah')
end
it "should say 'nah' if it is invalid" do
expect(myclass.process('<some><xml>here</some>')).to eq('nah')
end
it 'should check the context of the message' do
expect(myclass).to receive(:react_upon?).with('<some><xml>here</xml></some>')
myclass.process('<some><xml>here</xml></some>')
end
end
end
Right now your tests have no expectations so I added one that expects myclass to respiond_to the react_upon? method and another that expects myclass.process(xml) to respond with a String that equals yeah.

Issue stubbing with RSpec

I am trying to understand why the result of these tests, the first test claims the method is not stubbed, however, the 2nd one is.
class Roll
def initialize
install if !installed?
end
def install; puts 'install'; end
end
describe Roll do
before do
class RollTestClass < Roll; end
RollTestClass.any_instance.stub(:install)
end
let(:roll_class) { RollTestClass }
let(:roll) { RollTestClass.new }
context 'when installed is true' do
before do
roll_class.any_instance.stub(:installed?).and_return(true)
end
it 'should not call install' do
expect(roll).to_not have_received(:install)
end
end
context 'when installed is false' do
before do
roll_class.any_instance.stub(:installed?).and_return(false)
end
it 'should call install' do
expect(roll).to have_received(:install)
end
end
end
It's also strange the error says expected to have received install, but I think that is likely just faulty feedback from the RSpec DSL. But maybe worth noting.
1) Roll when installed is true should not call install
Failure/Error: expect(roll).to_not have_received(:install)
#<RollTestClass:0x10f69ef78> expected to have received install, but that method has not been stubbed.
The "spy pattern" of RSpec requires that the objects have been previously stubbed. However, any_instance.stub doesn't actually stub the methods "for real" unless/until the method is invoked on a particular object. As such, the methods appears as being "unstubbed" and you get the error you're getting. Here's some code that demonstrates the change in definition:
class Foo
end
describe "" do
it "" do
Foo.any_instance.stub(:bar)
foo1 = Foo.new
foo2 = Foo.new
print_bars = -> (context) {puts "#{context}, foo1#bar is #{foo1.method(:bar)}, foo2#bar is #{foo2.method(:bar)}"}
print_bars['before call']
foo1.bar
print_bars['after call']
end
end
which produces the following output:
before call, foo1#bar is #<Method: Foo#bar>, foo2#bar is #<Method: Foo#bar>
after call, foo1#bar is #<Method: #<Foo:0x007fc0c3842ef8>.bar>, foo2#bar is #<Method: Foo#bar>
I reported this an issue on RSpec's github site and got this acknowledgement/response.
You can use the following alternative approach, which depends on the recently introduced expect_any_instance_of method.
class Roll
def initialize
install if !installed?
end
def install; puts 'install'; end
end
describe Roll do
before do
class RollTestClass < Roll; end
end
let(:roll_class) { RollTestClass }
let(:roll) { RollTestClass.new }
context 'when installed is true' do
before do
roll_class.any_instance.stub(:installed?).and_return(true)
end
it 'should not call install' do
expect_any_instance_of(roll_class).to_not receive(:install)
roll
end
end
context 'when installed is false' do
before do
roll_class.any_instance.stub(:installed?).and_return(false)
end
it 'should call install' do
expect_any_instance_of(roll_class).to receive(:install)
roll
end
end
end

Passing an object as subject to rspec

I am running rspec tests on a catalog object from within a Ruby app, using Rspec::Core::Runner::run:
File.open('/tmp/catalog', 'w') do |out|
YAML.dump(catalog, out)
end
...
unless RSpec::Core::Runner::run(spec_dirs, $stderr, out) == 0
raise Puppet::Error, "Unit tests failed:\n#{out.string}"
end
(The full code can be found at https://github.com/camptocamp/puppet-spec/blob/master/lib/puppet/indirector/catalog/rest_spec.rb)
In order to pass the object I want to test, I dump it as YAML to a file (currently /tmp/catalog) and load it as subject in my tests:
describe 'notrun' do
subject { YAML.load_file('/tmp/catalog') }
it { should contain_package('ppet') }
end
Is there a way I could pass the catalog object as subject to my tests without dumping it to a file?
I am not very clear as to what exactly you are trying to achieve but from my understanding I feel that using a before(:each) hook might be of use to you. You can define variables in this block that are available to all the stories in that scope.
Here is an example:
require "rspec/expectations"
class Thing
def widgets
#widgets ||= []
end
end
describe Thing do
before(:each) do
#thing = Thing.new
end
describe "initialized in before(:each)" do
it "has 0 widgets" do
# #thing is available here
#thing.should have(0).widgets
end
it "can get accept new widgets" do
#thing.widgets << Object.new
end
it "does not share state across examples" do
#thing.should have(0).widgets
end
end
end
You can find more details at:
https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks/before-and-after-hooks#define-before(:each)-block

Resources