How to test method that delegates to the initiation of another class with rspec? - ruby

How would you go about testing this with rspec?
class SomeClass
def map_url(size)
GoogleMap.new(point: model.location.point, size: size).map_url
end
end

The fact that your test seems "very coupled and brittle to mock" is a sign that the code itself is doing too many things at once.
To highlight the problem, look at this implementation of map_url, which is meaningless (returning "foo" for any size input) and yet passes your tests:
class SomeClass
def map_url(size)
GoogleMap.new.map_url
GoogleMap.new(point: model.location.point, size: size)
return "foo"
end
end
Notice that:
A new map is being initiated with the correct arguments, but is not contributing to the return value.
map_url is being called on a newly-initiated map, but not the one initiated with the correct arguments.
The result of map_url is not being returned.
I'd argue that the problem is that the way you have structured your code makes it look simpler than it actually is. As a result, your tests are too simple and thus fall short of fully covering the method's behaviour.
This comment from David Chelimsky seems relevant here:
There is an old guideline in TDD that suggests that you should listen to
your tests because when they hurt there is usually a design problem.
Tests are clients of the code under test, and if the test hurts, then so
do all of the other clients in the codebase. Shortcuts like this quickly
become an excuse for poor designs. I want it to stay painful because it
should hurt to do this.
Following this advice, I'd suggest first splitting the code into two separate methods, to isolate concerns:
class SomeClass
def new_map(size)
GoogleMap.new(point: model.location.point, size: size)
end
def map_url(size)
new_map(size).map_url
end
end
Then you can test them separately:
describe SomeClass do
let(:some_class) { SomeClass.new }
let(:mock_map) { double('map') }
describe "#new_map" do
it "returns a GoogleMap with the correct point and size" do
map = some_class.new_map('300x600')
map.point.should == [1,2]
map.size.should == '300x600'
end
end
describe "#map_url" do
before do
some_class.should_receive(:new_map).with('300x600').and_return(mock_map)
end
it "initiates a new map of the right size and call map_url on it" do
mock_map.should_receive(:map_url)
some_class.map_url('300x600')
end
it "returns the url" do
mock_map.stub(map_url: "http://www.example.com")
some_class.map_url('300x600').should == "http://www.example.com"
end
end
end
The resulting test code is a longer and there are 3 specs rather than two, but I think it more clearly and cleanly separates the steps involved in your code, and covers the method behaviour completely. Let me know if this makes sense.

So this is how I did it, it feels very coupled and brittle to mock it like this. Suggestions?
describe SomeClass do
let(:some_class) { SomeClass.new }
describe "#map_url" do
it "should instantiate a GoogleMap with the correct args" do
GoogleMap.should_receive(:new).with(point: [1,2], size: '300x600') { stub(map_url: nil) }
some_class.map_url('300x600')
end
it "should call map_url on GoogleMap instance" do
GoogleMap.any_instance.should_receive(:map_url)
some_class.map_url('300x600')
end
end
end

Related

How to properly stub doubles

Code being tested:
class Session
def initialize
#interface = Interface.new(self)
#interface.hello
end
end
class Interface
def initialize(session, out = $STDOUT)
#session = session
#out = out
end
def hello
#out.puts "hello"
end
end
Test:
describe Session do
let (:fake_stdout) {double("$STDOUT", :puts => true)}
let (:interface) {instance_double("Interface", :out => "fake_stdout")}
let (:session) { Session.new }
describe "#new" do
it "creates an instance of Session" do
expect(session).to be_an_instance_of(Session)
end
end
end
This throws private method 'puts' called for nil:NilClass. It seems it's not seeing the fake_stdout with its specified :puts as out. I tried tying it with allow(Interface).to receive(:new).with(session).and_return(interface), but that changed nothing. How do I get the tested Session class to see the double/instance double and pass the test?
I think, this is not really problem with stubbing, but the general approach. When writing your unit tests for some class, you should stick to functionality of that class and eventually to API it sees. If you're stubbing "internal" out of Interface - it's already to much for specs of Session.
What Session really sees, is Interfaces public hello method, thus Session spec, should not be aware of internal implementation of it (that it is #out.puts "hello"). The only thing you should really focus is that, the hello method has been called. On the other hand, ensuring that the put is called for hello should be described in specs for Interface.
Ufff... That's long introduction/explanation, but how to proceed then? (known as show me the code! too ;)).
Having said, that Session.new should be aware only of Interfaces hello method, it should trust it works properly, and Sessions spec should ensure that the method is called. For that, we'll use a spy. Let's get our hand dirty!
RSpec.describe Session do
let(:fake_interface) { spy("interface") }
let(:session) { Session.new }
before do
allow(Interface).to receive(:new).and_return(fake_interface)
end
describe "#new" do
it "creates an instance of Session" do
expect(session).to be_an_instance_of(Session) # this works now!
end
it "calls Interface's hello method when initialized" do
Session.new
expect(fake_interface).to have_received(:hello)
end
end
end
A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.
This is taken from SinonJS (which is the first result when googling for "what is test spy"), but explanation is accurate.
How does this work?
Session.new
expect(fake_interface).to have_received(:hello)
First of all, we're executing some code, and after that we're asserting that expected things happened. Conceptually, we want to be sure, that during Session.new, the fake_interface have_received(:hello). That's all!
Ok, but I need another test ensuring that Interfaces method is called with specific argument.
Ok, let's test that!
Assuming the Session looks like:
class Session
def initialize
#interface = Interface.new(self)
#interface.hello
#interface.say "Something More!"
end
end
We want to test say:
RSpec.describe Session do
describe "#new" do
# rest of the code
it "calls interface's say_something_more with specific string" do
Session.new
expect(fake_interface).to have_received(:say).with("Something More!")
end
end
end
This one is pretty straightforward.
One more thing - my Interface takes a Session as an argument. How to test that the interface calls sessions method?
Let's take a look at sample implementation:
class Interface
# rest of the code
def do_something_to_session
#session.a_session_method
end
end
class Session
# ...
def another_method
#interface.do_something_to_session
end
def a_session_method
# some fancy code here
end
end
It won't be much surprise, if I say...
RSpec.describe Session do
# rest of the code
describe "#do_something_to_session" do
it "calls the a_session_method" do
Session.new.another_method
expect(fake_interface).to have_received(:do_something_to_session)
end
end
end
You should check, if Sessions another_method called interfaces do_something_to_session method.
If you test like this, you make the tests less fragile to future changes. You might change an implementation of Interface, that it doesn't rely on put any more. When such change is introduced - you have to update the tests of Interface only. Session knows only the proper method is called, but what happens inside? That's the Interfaces job...
Hope that helps! Please, take a look at another example of spy in my other answer.
Good luck!

Why are successive arrays collected from ObjectSpace not equal in my spec?

I have a Project class with two ObjectSpace-related methods:
def self.all
ObjectSpace.each_object(self).to_a
end
def self.count
ObjectSpace.each_object(self).count
end
This spec is failing:
it "can print all projects" do
Project.all.should eq([#project1, #project2])
end
with the following error:
Failure/Error: Project.all.should eq([#project1, #project2])
expected: [#<Project:0x007fd76a815508 #name="Building house", #tasks=[]>, #<Project:0x007fd76a815198 #name="Getting a loan from the Bank", #tasks=[]>]
got: [#<Project:0x007fd7688336b8 #name="Building house", #tasks=[]>, #<Project:0x007fd7688dae40 #name="Building house", #tasks=[]>, #<Project:0x007fd768af4de8 #name="Getting a loan from the Bank", #tasks=[]>, #<Project:0x007fd768af5090 #name="Building house", #tasks=[]>, #<Project:0x007fd76a815198 #name="Getting a loan from the Bank", #tasks=[]>, #<Project:0x007fd76a815508 #name="Building house", #tasks=[]>]
As you can see, this is giving me my objects in an array doubled but the code itself works fine. So why is my test failing?
Because previously-existing Projects still exist as objects.
This means they'll still be found in ObjectSpace, and you'll have more objects than you expect.
ObjectSpace May Contains Traces of Nuts
Well, not really. But ObjectSpace often contains objects that belong to other scopes, objects which have been marked for garbage collection but not yet removed, or (in the case of RSpec tests in particular) copies of objects from multiple invocations of a before block.
Ruby 2.0 may be different, but earlier MRI interpreters don't make guarantees about garbage collection, so you can't really count on the contents of ObjectSpace to be valid for an equality test even if you manually run GC.start.
Refactor Your Code
Instead of looking for equality in your spec, you might consider:
Looking for inclusion with Array#include?
Looking for specific object IDs within ObjectSpace.
Refactoring the class under test, as well as the test itself. Specifically, use an aggregation pattern of some sort rather than relying on ObjectSpace to hold references to the Project objects you care about.
You can mix-and-match here, but fixing your test is really only part of the problem. The underlying application logic seems to be in need of refactoring.
The failing test is good, in that it highlights a class that needs surgery. Don't just fix the test; listen to what the spec is trying to tell you about the class under test.
project_spec.rb
describe Project do
let(:p1) { Project.new }
let(:p2) { Project.new }
describe ".all" do
it "should keep track of all pr" do
Project.all.should == [p1, p2]
end
end
describe ".count" do
it "should count all the projects" do
Project.count.should == 2
end
end
end
project.rb
class Project
##all_projects = []
def initialize(options=nil)
##all_projects << self
end
def self.all
##all_projects
end
def self.count
##all_projects.count
end
end
Finished in 0.00079 seconds
2 examples, 0 failures
I'll leave it upto you to work out the other details that might be specific and intricate to your project. Hope this works out for you.

Ruby Double (RR) How do I set up expectations for a block of method calls passed to block argument method?

In my code I have code similar to the following contrived example.
class Excel
def self.do_tasks
with_excel do |excel|
delete_old_exports
export_images(excel)
export_documents(excel)
end
end
def with_excel
excel = WIN32OLE.connect('Excel.Application')
begin
yield excel
ensure
excel.close()
end
end
end
Now, I want to write a test for the 'do_tasks' method, where I set up expectations for the method calls and see if those expectations are fulfilled.
I tried the following approach (with shoulda-context and test-unit). However,the expectations fail for the three last mocks (the mocks do not get called).
class ExcelTest < ActiveSupport::TestCase
should "call the expected methods" do
mock.proxy(Excel).with_excel
mock(Excel).delete_old_exports
mock(Excel).export_images.with_any_args
mock(Excel).export_documents.with_any_args
Excel.do_tasks
end
end
Any pointers on how to test this sort of code would be much appreciated!
An older question, but I've just been doing some work on some similar code with rr and thought I'd throw in an answer.
The following test will do what you asked (using RR and TestUnit):
describe Excel do
describe '.do_tasks' do
let(:excel_ole) { mock!.close.subject }
before do
stub(WIN32OLE).connect('Excel.Application') { excel_ole }
mock(Excel).delete_old_exports
mock(Excel).export_images(excel_ole)
mock(Excel).export_documents(excel_ole)
end
it 'calls the expected methods' do
Excel.do_tasks
assert_received(Excel) { |subject| subject.delete_old_exports }
end
end
end
It uses RR's "spy" doubles - see https://github.com/rr/rr#spies
However, in the case of the sample code you provided, the fact that the methods you want to test are inside a block is an implementation detail and shouldn't be implicitly tested (this can lead to brittle tests). The test above shows this, the with_excel method is not mocked (incidentally, this should be defined as self.with_excel for the code to work). The implementation could be refactored so that the WIN32OLE initialisation and teardown happens inline in the .do_tasks method and the test would still pass.
On another note, it may be a side effect of the contrived example, but in general it's a bad idea to test non-public methods. The methods delete_old_exports, export_images and export_documents look like they should perhaps be factored out to collaborators.

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

A ruby method to replace "="

I want to eliminate "=" sign for a particular reason. It might looks like this:
cat_that_has_name("Kelly").as(:kelly)
kelly.do_something
The "as" method here is used to generate a method "kelly" that reference my cat. Could anyone help me with this?
Any suggestions will be appreciated.
Update:
Jorg was right, I've add a simple test to demonstrate my intention:
require "test/unit"
class AsTest < Test::Unit::TestCase
def setup
#cats = ["Kelly", "Tommy"]
end
def teardown
end
def test_as
kelly1 = get_cat("Kelly")
get_cat("Kelly").as(:kelly2)
assert_equal(kelly1.object_id, kelly2.object_id)
end
private
def get_cat(name)
#cats.each do |cat|
if cat.to_s==name
return cat
end
end
return nil
end
end
It's kind of hard to figure out what you actually want. If you want some sensible answers, you will have to provide a complete code example of what you want to achieve (for example, the code you posted is missing definitions for the cat_that_has_name and so_something methods). You will also need to post a complete specification of what exactly you expect the as method to do, with usage examples and ideally also with a testsuite. After all, how do we know if our answer is correct if you haven't defined what "correct" means?
The best I could decipher from your cryptic question is something like this:
class Object
def as(name)
s = self
Object.send(:define_method, name) { s }
Object.send(:private, name)
end
end
But there is no way of knowing whether this works, because if I try to run your code example, I get a NoMethodError for cat_that_has_name and another NoMethodError for so_something.
Note also that your question is self-inconsistent: in your subject line you ask about a method to replace = (i.e. creating variables) but in your question you talk about creating methods, which would mean that you are looking for a replacement for def and not for =. Again, it would be much easier to answer correctly if there were a testsuite.

Resources