Lately I’ve been playing around with RSpec, which is a fantastic testing tool that falls under the umbrella of what’s being called “Behavior Driven Development” (BDD). What BDD advocates is that there should be a common language used to talk about your system, and that describing the behaviors of the system ensures that you are delivering what’s really valuable, instead of writing lots of code that may not be necessary. If you are interested in learning more about BDD, I suggest you head over here and start reading, as it seems to be the resource to go to for more information. Anyway, I’m not here to talk about some quirks with RSpec in the hope that someone will stumble over here and provide some insight. Let’s start with some code and work from there towards my conclusions:
Here is a simple class representing a digital photo that you might store on a server, your computer, wherever. This is as simplified as I want it to be for the purposes of illustrating my points.
#!/usr/bin/ruby
class Photo
attr_reader :height, :width
def initialize( width, height )
@width = width
@height = height
end
def scale_absolute( width, height )
@width = width
@height = height
end
def scale_percentage( width, height )
@width *= ( width / 100.0 )
@height *= ( height / 100.0 )
end
end
Our second code file is just a module of helper methods that I’ve creatively titled HelperMethods
module HelperMethods
def nice_photo_resolution( photo )
if photo.respond_to?( :width ) && photo.respond_to?( :height )
return "#{photo.width}x#{photo.height}"
end
end
end
And here is our rspec code for testing the Photo class. We just simply use the describe method to create an ExampleGroup instance, setup some basic values, have a before method to instantiate an instance of a Photo, and then our it methods which create Example instances to be run.
#!/usr/bin/ruby
require 'photo'
require 'helper_methods'
describe Photo do
include HelperMethods
START_HEIGHT = rand(640)
START_WIDTH = rand(480)
before( :each ) do
@photo = Photo.new( START_WIDTH, START_HEIGHT )
end
it "should resize a photo from #{START_WIDTH}x#{START_HEIGHT}to 640x480" do
@photo.scale_absolute( 640, 480 )
@photo.width.should == 640
@photo.height.should == 480
end
it "should scale a photo 200% from #{START_WIDTH}x#{START_HEIGHT} to #{START_WIDTH * 2}x#{START_HEIGHT * 2}" do
@photo.scale_percentage( 200, 200 )
@photo.width.should == ( START_WIDTH * 2 )
@photo.height.should == ( START_HEIGHT * 2 )
end
it "should scale a photo 50% from #{START_WIDTH}x#{START_HEIGHT} to #{( START_WIDTH * 0.50 )}x#{( START_HEIGHT * 0.50)}" do
@photo.scale_percentage( 50, 50 )
@photo.width.should == ( START_WIDTH * 0.50 )
@photo.height.should == ( START_HEIGHT * 0.50 )
end
it "should rotate a photo 90 degrees such that the dimensions change from #{START_WIDTH}x#{START_HEIGHT} to #{START_HEIGHT}x#{START_WIDTH}" do
@photo.rotate( 90 )
@photo.width.should == START_HEIGHT
@photo.height.should == START_WIDTH
end
end
Now, as I’ve been playing around, I’ve noticed several things that have stuck in my craw. The first is that even though I’m including HelperMethods in the describe block, if I try to use it in the description argument passed to the it method, I get an error trying to run my spec test telling me that no such method exists. Take a look here to see what I mean:
describe Photo do
include HelperMethods
START_HEIGHT = rand(640)
START_WIDTH = rand(480)
before( :each ) do
@photo = Photo.new( START_WIDTH, START_HEIGHT )
end
it "should resize a photo from #{nice_photo_resolution( @photo )}to 640x480" do
@photo.scale_absolute( 640, 480 )
@photo.width.should == 640
@photo.height.should == 480
end
...
end
When I try to run the spec command on that ExampleGroup, I get an error saying the nice_photo_resolution method cannot be found. This leads me to believe RSpec is doing something with the it method before it builds the ExampleGroup instance from the describe method. Further supporting this theory is that when you try to instantiate instance level variables in a before method, they cannot be seen and are evaluated as nil in the code as its executed. Take a look here for an example:
describe Photo do
include HelperMethods
before( :each ) do
@start_width = rand( 640 )
@start_height = rand( 480 )
@photo = Photo.new( @start_width, @start_height )
end
it "should resize a photo from #{@start_width}x#{@start_height}to 640x480" do
@photo.scale_absolute( 640, 480 )
@photo.width.should == 640
@photo.height.should == 480
end
...
end
That example is a little contrived, but I think it gets the point across. Moving to a more realistic example, this produces the same error:
describe Photo do
include HelperMethods
before( :each ) do
@photo = Photo.new( rand( 640 ), rand( 480 ) )
end
it "should resize a photo from #{@photo.width}x#{@photo.height}to 640x480" do
@photo.scale_absolute( 640, 480 )
@photo.width.should == 640
@photo.height.should == 480
end
...
end
This will cause the spec command to throw an error, because @photo is evaluating to nil, and trying to access the width or height instance fields of the object cause an error to be thrown.
I’m not entirely sure that my imperfect understanding of Ruby class instantiation is at fault for my confusion, but it doesn’t seem like it should be the case. I also haven’t cracked open the RSpec code yet, as I am just trying to learn the system now and don’t want to have to dive into the gory details until I understand how to use it to get work done. If anyone can point me in the right direction though I would truly appreciate it. These examples are fairly unrealistic, but I think it’s a short jump to seeing how not understanding the order of construction could lead to some problems when working on production-level code. I’d rather be testing my code than testing the RSpec system.
0 comments ↓
There are no comments yet...Kick things off by filling out the form below.
Leave a Comment