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

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.

Related

Unit testing with and without requiring ActiveSupport

I've extracted a single class from a Rails app into a gem. It's very, very simple, but of course I'd like to fully test it (I'm using rspec).
The class does some simple date-calculation. It's not dependent on Rails, but since it started out in a Rails app, and is still used there, it uses ActiveSupport's time zone-aware methods when it can. But, if ActiveSupport isn't available, it should use the std-lib Date methods.
Specifically, it only does this in one single place: Defaulting an optional argument to "today's date":
arg ||= if Date.respond_to?(:current)
Date.current # use ActiveSupport's time zone-aware mixin if possible
else
Date.today # stdlib fallback
end
Question is: How do I properly test this? If I require ActiveSupport in my spec_helper.rb, it'll obviously always use that. If I don't require it anywhere, it'll never use it. And if I require it for a single example group, rspec's random execution order makes the testing unpredictable, as I don't know when AS will be required.
I can require maybe it in a before(:all) in a nested group, as nested groups are (I believe) processed highest to deepest. But that seems terribly inelegant.
I could also split the specs into two files, and run them separately, but again, that seems unnecessary.
I could also disable rspec's random ordering, but that's sort of going against the grain. I'd rather have it as randomized as possible.
Any ideas?
Another solution is to mock the current and today methods, and use those for testing. Eg:
# you won't need these two lines, just there to make script work standalone
require 'rspec'
require 'rspec/mocks/standalone'
def test_method(arg = nil)
arg ||= if Date.respond_to?(:current)
Date.current # use ActiveSupport's time zone-aware mixin if possible
else
Date.today # stdlib fallback
end
arg
end
describe "test_method" do
let(:test_date) { Date.new(2001, 2, 3) }
it "returns arg unchanged if not nil" do
test_method(34).should == 34
end
context "without Date.current available" do
before(:all) do
Date.stub(:today) { test_date }
end
it "returns Date.today when arg isn't present" do
test_method.should == test_date
end
end
context "with Date.current available" do
before(:all) do
Date.stub(:current) { test_date }
end
it "returns Date.current when arg isn't present" do
test_method.should == test_date
end
end
end
Running with rspec test.rb results in the tests passing.
Also, the stubs are present only in each context, so it doesn't matter what order the specs are run in.
This is more than a little perverse, but it should work. Include ActiveSupport, and then:
context "without ActiveSupport's Date.current" do
before(:each) do
class Date
class << self
alias_method :current_backup, :current
undef_method :current
end
end
end
# your test
after(:each) do
class Date
class << self
alias_method :current, :current_backup
end
end
end
end
I can't really recommend this; I would prefer to split out this one spec and run it separately as you suggested.

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

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

Is this Ruby class really badly designed?

I'm quite new to OOP and I'm concerned that this class that I've written is really poorly designed. It seems to disobey several principles of OOP:
It doesn't contain its own data, but relies on a yaml file for
values.
Its methods need to be called in a particular order
It has a lot of instance variables and methods
It does work, however. It's robust, but I'll need to modify the source code to add new getter methods every time I add page elements
It's a model of an html document used in an automated test suite. I keep thinking that some of the methods could be put in subclasses, but I'm concerned that I'd have too many classes then.
What do you think?
class BrandFlightsPage < FlightSearchPage
attr_reader :route, :date, :itinerary_type, :no_of_pax,
:no_results_error_container, :submit_button_element
def initialize(browser, page, brand)
super(browser, page)
#Get reference to config file
config_file = File.join(File.dirname(__FILE__), '..', 'config', 'site_config.yml')
#Store hash of config values in local variable
config = YAML.load_file config_file
#brand = brand #brand is specified by the customer in the features file
#Define instance variables from the hash keys
config.each do |k,v|
instance_variable_set("##{k}",v)
end
end
def visit
#browser.goto(#start_url)
end
def set_origin(origin)
self.text_field(#route[:attribute] => #route[:origin]).set origin
end
def set_destination(destination)
self.text_field(#route[:attribute] => #route[:destination]).set destination
end
def set_departure_date(outbound)
self.text_field(#route[:attribute] => #date[:outgoing_date]).set outbound
end
def set_journey_type(type)
if type == "return"
self.radio(#route[:attribute] => #itinerary_type[:single]).set
else
self.radio(#route[:attribute] => #itinerary_type[:return]).set
end
end
def set_return_date(inbound)
self.text_field(#route[:attribute] => #date[:incoming_date]).set inbound
end
def set_number_of_adults(adults)
self.select_list(#route[:attribute] => #no_of_pax[:adults]).select adults
end
def set_no_of_children(children)
self.select_list(#route[:attribute] => #no_of_pax[:children]).select children
end
def set_no_of_seniors(seniors)
self.select_list(#route[:attribute] => #no_of_adults[:seniors]).select seniors
end
def no_flights_found_message
#browser.div(#no_results_error_container[:attribute] => #no_results_error_container[:error_element]).text
raise UserErrorNotDisplayed, "Expected user error message not displayed" unless divFlightResultErrTitle.exists?
end
def submit_search
self.link(#submit_button_element[:attribute] => #submit_button_element[:button_element]).click
end
end
If this class is designed as a Facade, then it's not (too) bad design. It provides a coherent unified way to perform related operations that rely on a variety of un-related behavior holders.
It appears to be poor separation of concerns, in that this class essentially coupling all the various implementation details, which might turn out to be somewhat tricky to maintain.
Finally, the fact methods need to be called in a specific order may hint at the fact you're trying to model a state machine - in which case it probably should be broken down to several classes (one per "state"). I don't think there's a "too many methods" or "too many classes" point you'd reach, the fact is you need the features provided by each class to be coherent and making sense. Where to draw the line is up to you and your specific implementation's domain requirements.

How display failure on undescribed example in RSpec?

I am describing class on RSpec
class Pupil
def initialize(name, dateOfBirth)
#name = name
#dateOfBirth = dateOfBirth
end
def name
#name
end
def ages
#should calculate ages
end
end
describe Pupil do
context do
pupil = Pupil.new("Stanislav Majewski", "1 april 1999")
it "should returns name" do
pupil.name.should eq("Stanislav Majewski")
end
it "should calculates ages" do
#not described
end
end
end
RSpec returns:
..
Finished in 0.00203 seconds
2 examples, 0 failures
Is there an elegant way to display a failure message that the method is not described?
If you're concerned that you'll create a test and forget to put anything it in (sometimes I'll create three tests I know I'll need, and work on each of them in turn) then you can do the following:
it "should calculates ages" do
fail
end
OR
it "should calculates ages"
...and that's all (no block) will mark the test as pending automatically. In other words, don't fill out your tests until they have actual test code in them.
Also, if you don't test any assertions (i.e. if your spec doesn't contain any lines that have a call to should in them), your spec will appear to pass. This has happened to me a few times, where I write a new test, expecting it to fail, and it doesn't because I forgot to include the call to should which is what actually tests the assertion.

How do I effectively force Minitest to run my tests in order?

I know. This is discouraged. For reasons I won't get into, I need to run my tests in the order they are written. According to the documentation, if my test class (we'll call it TestClass) extends Minitest::Unit::TestCase, then I should be able to call the public method i_suck_and_my_tests_are_order_dependent! (Gee - do you think the guy who created Minitest had an opinion on that one?). Additionally, there is also the option of calling a method called test_order and specifying :alpha to override the default behavior of :random. Neither of these are working for me.
Here's an example:
class TestClass < Minitest::Unit::TestCase
#override random test run ordering
i_suck_and_my_tests_are_order_dependent!
def setup
...setup code
end
def teardown
...teardown code
end
def test_1
test_1 code....
assert(stuff to assert here, etc...)
puts 'test_1'
end
def test_2
test_2_code
assert(stuff to assert here, etc...)
puts 'test_2'
end
end
When I run this, I get:
undefined method `i_suck_and_my_tests_are_order_dependent!' for TestClass:Class (NoMethodError)
If I replace the i_suck method call with a method at the top a la:
def test_order
:alpha
end
My test runs, but I can tell from the puts for each method that things are still running in random order each time I run the tests.
Does anyone know what I'm doing wrong?
Thanks.
If you just add test_order: alpha to your test class, the tests will run in order:
class TestHomePage
def self.test_order
:alpha
end
def test_a
puts "a"
end
def test_b
puts "b"
end
end
Note that, as of minitest 5.10.1, the i_suck_and_my_tests_are_order_dependent! method/directive is completely nonfunctional in test suites using MiniTest::Spec syntax. The Minitest.test_order method is apparently not being called at all.
EDIT: This has been a known issue since Minitest 5.3.4: see seattlerb/minitest#514 for the blow-by-blow wailing and preening.
You and I aren't the ones who "suck". What's needed is a BDD specification tool for Ruby without the bloat of RSpec and without the frat-boy attitude and contempt for wider community practices of MiniTest. Does anyone have any pointers?
i_suck_and_my_tests_are_order_dependent! may be a later addition to minitest & not available as a Ruby core method. In that case, you'd want to force use of your gem version:
require 'rubygems'
gem 'minitest'
I think that the method *test_order* should be a class method and not a instance method like so:
# tests are order dependent
def self.test_order
:alpha
end
The best way to interfere in this chain may be to override a class method runnable_methods:
def self.runnable_methods
['run_first'] | super | ['run_last']
end
#Minitest version:
def self.runnable_methods
methods = methods_matching(/^test_/)
case self.test_order
when :random, :parallel then
max = methods.size
methods.sort.sort_by { rand max }
when :alpha, :sorted then
methods.sort
else
raise "Unknown test_order: #{self.test_order.inspect}"
end
end
You can reorder test any suitable way around. If you define your special ordered tests with
test 'some special ordered test' do
end
, don't forget to remove them from the results of super call.
In my example I need to be sure only in one particular test to run last, so I keep random order on whole suite and place 'run_last' at the end of it.

Resources