Making specs more concise (Intermediate)

Exclusive offer: get 50% off this eBook here
Instant RSpec Test-Driven Development How-to [Instant]

Instant RSpec Test-Driven Development How-to [Instant] — Save 50%

Learn RSpec and redefine your approach towards software development with this book and ebook

£8.99    £4.50
by Charles Feduke | September 2013 | Open Source

This article written by, Charles Feduke, the author of Instant RSpec Test-Driven Development How-to, demonstrates idiomatic RSpec code that makes good use of the RSpec Domain Specific Language (DSL).

(For more resources related to this topic, see here.)

Making specs more concise (Intermediate)

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.

How to do it...

  1. Refactor our specs in 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

  2. While running the spec, you see a clean output because we've separated multiple assertions into their own specifications:

    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.

  3. It is time to add something meatier. As part of our project, we'll want to determine if Location is within a certain mile radius of another point.
  4. In 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

  5. Because we've just refactored, we'll execute rspec and see the specs pass.
  6. Now, let's spec out a 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

  7. Running rspec now results in failure because there's no Location#near? method defined.
  8. The following is the naive implementation that passes the test (in lib/location.rb):

    def near?(latitude, longitude, mile_radius) true end

  9. Now, we can drive a failure case, which will force a real implementation in 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

  10. Running the specs now results in the expected failure.
  11. The following is a passing implementation of the haversine formula in 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

  12. Refactor both of the previous tests to be more expressive by utilizing predicate matchers:

    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

  13. Now that we have a passing spec for #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

  14. Finally, run rspec again and see that the tests continue to pass. A successful refactor!

How it works...

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" }

There's more...

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.

Summary

This article demonstrated to us the idiomatic RSpec code that makes good use of the RSpec Domain Specific Language (DSL).

Resources for Article :


Further resources on this subject:


Instant RSpec Test-Driven Development How-to [Instant] Learn RSpec and redefine your approach towards software development with this book and ebook
Published: June 2013
eBook Price: £8.99
See more
Select your format and quantity:

About the Author :


Charles Feduke

Charles Feduke began developing software in Perl nearly 2 decades ago. He was trapped in the Microsoft platform for far too long and spends his free time these days writing Ruby, learning Scala, and wishing he was really serious about writing C during the 90s.

Books From Packt


Building Dynamic Web 2.0 Websites with Ruby on Rails
Building Dynamic Web 2.0 Websites with Ruby on Rails

Aptana RadRails: An IDE for Rails Development
Aptana RadRails: An IDE for Rails Development

Ruby on Rails Web Mashup Projects
Ruby on Rails Web Mashup Projects

Ruby on Rails Enterprise Application Development: Plan, Program, Extend
Ruby on Rails Enterprise Application Development: Plan, Program, Extend

RubyMotion iOS Development Essentials
RubyMotion iOS Development Essentials

Cloning Internet Applications with Ruby
Cloning Internet Applications with Ruby

Ruby and MongoDB Web Development Beginner's Guide
Ruby and MongoDB Web Development Beginner's Guide

Instant RubyMotion App Development
Instant RubyMotion App Development


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software