





















































(For more resources related to this topic, see here.)
So far, we've written specifications that work in the spirit of unit testing, but we're not yet taking advantage of any of the important features of RSpec to make writing tests more fluid. The specs illustrated so far closely resemble unit testing patterns and have multiple assertions in each spec.
spec/lib/location_spec.rb
to make them more concise:
require "spec_helper" describe Location do describe "#initialize" do subject { Location.new(:latitude => 38.911268, :longitude => -77.444243) } its (:latitude) { should == 38.911268 } its (:longitude) { should == -77.444243 } end end
Location #initialize latitude should == 38.911268 longitude should == -77.444243 Finished in 0.00058 seconds 2 examples, 0 failures
The preceding output requires either the .rspec
file to contain the --format doc
line, or when executing rspec in the command line, the --format doc
argument must be passed. The default output format will print dots (.
) for passing tests, asterisks (*
) for pending tests, E
for errors, and F
for failures.
Location
is within a certain mile radius of another point.spec/lib/location_spec.rb
, we'll write some tests, starting with a new block called context
. The first spec we want to write is the happy path test. Then, we'll write tests to drive out other states. I am going to re-use our Location
instance for multiple examples, so I'll refactor that into another new construct, a let
block:
require "spec_helper" describe Location do let(:latitude) { 38.911268 } let(:longitude) { -77.444243 } let(:air_space) { Location.new(:latitude => 38.911268,: longitude => -77.444243) } describe "#initialize" do subject { air_space } its (:latitude) { should == latitude } its (:longitude) { should == longitude } end end
rspec
and see the specs pass.Location#near?
method by writing the code we wish we had:
describe "#near?" do context "when within the specified radius" do subject { air_space.near?(latitude, longitude, 1) } it { should be_true } end end end
rspec
now results in failure because there's no Location#near?
method defined.in lib/location.rb
):
def near?(latitude, longitude, mile_radius) true end
spec/lib/location_spec.rb
within the describe "#near?"
block:
context "when outside the specified radius" do subject { air_space.near?(latitude * 10, longitude * 10, 1) } it { should be_false } end
lib/location.rb
that satisfies both cases:
R = 3_959 # Earth's radius in miles, approx def near?(lat, long, mile_radius) to_radians = Proc.new { |d| d * Math::PI / 180 } dist_lat = to_radians.call(lat - self.latitude) dist_long = to_radians.call(long - self.longitude) lat1 = to_radians.call(self.latitude) lat2 = to_radians.call(lat) a = Math.sin(dist_lat/2) * Math.sin(dist_lat/2) + Math.sin(dist_long/2) * Math.sin(dist_long/2) * Math.cos(lat1) * Math.cos(lat2) c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) (R * c) <= mile_radius end
describe "#near?" do context "when within the specified radius" do subject { air_space } it { should be_near(latitude, longitude, 1) } end context "when outside the specified radius" do subject { air_space } it { should_not be_near(latitude * 10, longitude * 10, 1) } end end
#near?
, we can alleviate a problem with our implementation. The #near?
method is too complicated. It could be a pain to try and maintain this code in future. Refactor for ease of maintenance while ensuring that the specs still pass:
R = 3_959 # Earth's radius in miles, approx def near?(lat, long, mile_radius) loc = Location.new(:latitude => lat,:longitude => long) R * haversine_distance(loc) <= mile_radius end private def to_radians(degrees) degrees * Math::PI / 180 end def haversine_distance(loc) dist_lat = to_radians(loc.latitude - self.latitude) dist_long = to_radians(loc.longitude - self.longitude) lat1 = to_radians(self.latitude) lat2 = to_radians(loc.latitude) a = Math.sin(dist_lat/2) * Math.sin(dist_lat/2) +Math.sin(dist_long/2) * Math.sin(dist_long/2) *Math.cos(lat1) * Math.cos(lat2) 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)) end
rspec
again and see that the tests continue to pass. A successful refactor!The subject
block takes the return statement of the block—a new instance of Location
in the previous example—and binds it to a locally scoped variable named subject
. Subsequent it
and its
blocks can refer to that subject
variable. Furthermore, the its
blocks implicitly operate on the subject
variable to produce more concise tests.
Here is an example illustrating how subject
is used to produce easier-to-read tests:
describe "Example" do subject { { :key1 => "value1", :key2 => "value2" } } it "should have a size of 2" do subject.size.should == 2 end end
We can use subject
from within the it
block and this will refer to the anonymous hash returned by the subject
block. In the preceding test, we could have been more concise with an its
block:
its (:size) { should == 2 }
We're not limited to just sending symbols to an its
block—we can use strings too:
its ('size') { should == 2 }
When there is an attribute of subject
you want to assert but the value cannot easily be turned into a valid Ruby symbol, you'll need to use a string. This string is not evaluated as Ruby code; it's only evaluated against the subject under test as a method of that class.
Hashes, in particular, allow you to define an anonymous array with the key value to assert the value for that key:
its ([:key1]) { should == "value1" }
In the previous code examples, another block known as the context
block was presented. The context
block is a grouping mechanism for associating tests. For example, you may have a conditional branch in your code that changes the outputs of a method. Here, you may use two context
blocks, one for a value and the second for another value. In our example, we're separating the happy path (when a given point is within the specified mile radius) from the alternative (when a given point is outside the specified mile radius). context
is a useful construct that allows you to declare let
and other blocks within it, and those blocks apply only for the scope of the containing context
.
This article demonstrated to us the idiomatic RSpec code that makes good use of the RSpec Domain Specific Language (DSL).
Further resources on this subject: