Getting started with RSpec in Rails 3.2

Installing RSpec with Rails is pretty straight forward. The only task left since Bundler is to add RSpec to the Gemfile. And there is more good news, there is a special RSpec gem for Rails which has some handy shortcuts ready for you, I’ll be showing you some of those in future blog entries.

Quickest way is to append the gem to the Gemfile using the Console or Terminal just execute the following command in your project root directory(1):


echo "gem "rspec-rails", :group => [:test, :development]" >> Gemfile

Once you appended the RSpec gem install the gem via bundler i.e. bundle install.

We now got a new Rails generator commands:


Rspec:
  rspec:install

So lets install RSpec for our Rails project by running the following command rails g rspec:install

This will setup the RSpec environment in your Rails directory. Now we are ready to start cranking out RSpec tested code.

You may have noticed that we did not only add the gem to the test group but additionally to the development. You may ask yourself why? Well by adding it to the development group we get some more RSpec goodness. If you now use e.g.:


rails g model Something some_name some_description
      invoke  active_record

      create    db/migrate/20120207071500_create_somethings.rb
      create    app/models/something.rb
      invoke    rspec
      create      spec/models/something_spec.rb

Your Rails generator will automatically generate you the RSpec spec file. Let the BDD begin..

Sources:

RSpec book
Railscasts
Tekpub Rails 3 intro


(1)Or just simply open the Gemfile in the editor of choice and add the line: gem "rspec-rails", :group => [:test, :development]

Advertisements

Ruby Unit Tests breaking dependencies

One definition of a unit test is that it has to run fast or in the words of Michael Feathers in his book “Working Effectively with Legacy Code”:

A unit test that takes 1/10th of a second to run is a slow unit test.

If all operations happen in memory they tend to work rather quickly.(1)
This changes when we start to use files or network connections in classes that we put under unit tests. IO is costly as every roundtrip to the hard drive uses compared to RAM access forever LINK COMPARISON HDD TO RAM. So lets say we want to have a Ruby class that lists all entries of a directory and puts it into a nicely formatted string ready to print out to the console:


class DirLister
  def list_dir( path )
    raise ArgumentError, "Path #{path} does not exist." unless Dir.exists?( path )

    Dir.entries( path )
  end
end

To avoid the hit in execution time of our tests consider using default parameters for your IO classes in the constructor. So we would have to change our little dummy class to the following:


class DirLister
  @directory #defaults to Rubys Dir, see constructor

  def initialize( directory = Dir )
    @directory = directory
  end

  def list_dir( path )
    raise ArgumentError, "Path #{path} does not exist." unless @directory.exists?( path )

    @directory.entries( path )
  end
end

With these changes our test can now pass in a stub or if needed even a mock object. So how would we do this? Well remember Ruby is dynamic so if it quacks, walks and looks like a duck your all good when it comes to stubbing a object in Ruby. Here is an example of how the original MiniTest Spec could look like:


describe DirLister do
  it "lists all entries of directory" do
    test_path = "/tmp/test_path"
    expected_result = ["one", "two", "three"]

    DirLister.new().list_dir(test_path).must_equal expected_result
  end
end

Now if we wanted to pass in a fake dir object we could use the built in Mock from MiniTest which would result in the following code:

describe DirLister do
  it "lists all entries of directory" do
    test_path = "/tmp/test_path"
    expected_result = ["one", "two", "three"]

    dir_mock = MiniTest::Mock.new

    #MockMethod   MethodName, ReturnValue, Parameter(s)
    dir_mock.expect(:exists?, true, [test_path])
    dir_mock.expect(:entries, expected_result, [test_path])

    DirLister.new(dir_mock).list_dir(test_path).must_equal expected_result
  end
end

This approach of passing in system classes to your class can also be used to replace the Time class to simulate different times etc. as the current time given on the system.

One may argue that using a Mock class in this case is overkill. Another alternative would be to pass in a fake class object that would behave the same way we set up the MiniTest mock class.

(1) There are of course algorithms or big datablocks that tend to take more time. Consider in those cases to only tests aspects of the algorithm and move the more complex and time consuming test to your integration tests.