Advertisement

Ruby for Newbies: Testing with Rspec

by

Ruby is a one of the most popular languages used on the web. We're running a Session here on Nettuts+ that will introduce you to Ruby, as well as the great frameworks and tools that go along with Ruby development. In this episode, you’ll learn about testing your Ruby code with Rspec, one of the best testing libraries in the business.


Prefer a Screencast?


Look Familiar?

If you’ve read my recent tutorial on JasmineJS, you’ll probably notice several similarities in Rspec. Actually, the similarities are in Jasmine: Jasmine was created with Rspec in mind. We’re going to look at how to can use Rspec to do TDD in Ruby. In this tutorial, we’ll be creating some contrived Ruby classes to get us familiar with the Rspec syntax. However, the next “Ruby for Newbies” episode will feature using Rspec in conjunction withe some other libraries to test web apps … so stay tuned!


Setting Up

It’s pretty easy to install Rspec. Pop open that command line and run this:

gem install rspec

That easy.

Now, let’s set up a small project. We’re going to create two classes: Book and Library. Our Book objects will just store a title, author, and category. Our Library object will store a list of books, save them to a file, and allow us to fetch them by category.

Here’s what your project directory should look like:

Project Directory

We put the specifications (or specs) in a spec folder; we have one spec file for each class. Notice the spec_helper.rb file. For our specs to run, we need to require the Ruby classes we’re testing. That’s what we’re doing inside the spec_helper file:

require_relative '../library'
require_relative '../book'

require 'yaml'

(Have you met require_relative yet? No? Well, require_relative is just like require, except that instead of searching your Ruby path, it searches relative to the current directory.)

You may not be familiar with the YAML module; YAML is a simple text database that we’ll use to store data. You’ll see how it works, and we’ll talk more about it later.

So, now that we’re all set up, let’s get cracking on some specs!


The Book Class

Let’s start with the tests for the Book class.

require 'spec_helper'

describe Book do

end

This is how we start: with a describe block. Our parameter to describe explains what we’re testing: this could be a string, but in our case we’re using the class name.

So what are we going to put inside this describe block?

before :each do
    @book = Book.new "Title", "Author", :category
end

We’ll begin by making a call to before; we pass the symbol :each to specifc that we want this code run before each test (we could also do :all to run it once before all tests). What exactly are we doing before each test? We’re creating an instance of Book. Notice how we’re making it an instance variable, by prepending the variable name with @. We need to do this so that our variable will be accessible from within our tests. Otherwise, we’ll just get a local variable that’s only good inside the before block … which is no good at all.

Moving on,

describe "#new" do
    it "takes three parameters and returns a Book object" do
        @book.should be_an_instance_of Book
    end
end

Here’s our first test. We’re using a nested describe block here to say we’re describing the actions of a specific method. You’ll notice I’ve used the string “#new”; it’s a convention in Ruby to talk refer to instance methods like this: ClassName#methodName Since we have the class name in our top-level describe, we’re just putting the method name here.

Our test simply confims that we’re indeed made a Book object.

Notice the grammar we use here: object.should do_something. Ninety-nine percent of your tests will take this form: you have an object, and you start by calling should or should_not on the object. Then, you pass to that object the call to another function. In this case that’s be_an_instance_of (which takes Book as its single parameter). Altogether, this makes a perfectly readable test. It’s very clear that @book should be an instance of the class Book. So, let’s run it.

Open your terminal, cd into the project directory, and run rspec spec. The spec is the folder in which rspec will find the tests. You should see output saying something about “uninitialized constant Object::Book”; this just means there’s no Book class. Let’s fix that.

According to TDD, we only want to write enough code to fix this problem. In the book.rb file, that would be this:

class Book

end

Re-run the test (rspec spec), and you’ll find it’s passing fine. We don’t have an initialize method, so calling Ruby#new has no effect right now. But, we can create Book objects (albeit hollow ones.) Normally, we would follow this process through the rest of our development: write a test (or a few related tests), watch it fail, make it pass, refactor, repeat. However, for this tutorial, I’ll just show you the tests and code, and we’ll discuss them.

So, more tests for Book:

describe "#title" do
    it "returns the correct title" do
        @book.title.should eql "Title"
    end
end
describe "#author" do
    it "returns the correct author" do
        @book.author.should eql "Author"
    end
end
describe "#category" do
    it "returns the correct category" do
        @book.category.should eql :category
    end
end

There should all be pretty strightforward to you. But notice how we’re comparing in the test: with eql. There are three ways to test for equality with Rspec: using the operator == or the method eql both return true if the two objects have the same content. For example, both are strings or symbols that say the same thing. Then there’s equal, which only returns true in the two objects are really and truely equal, meaning they are the same object in memory. In our case, eql (or ==) is what we want.

These will fail, so here’s the code for Book to make them pass:

class Book
    attr_accessor :title, :author, :category
        def initialize title, author, category
            @title = title
            @author = author
            @category = category
        end 
end

Let’s move on to Library!


Speccing out the Library class

This one will be a bit more complicated. Let’s start with this:

require 'spec_helper'

describe "Library object" do

    before :all do
        lib_obj = [
            Book.new("JavaScript: The Good Parts", "Douglas Crockford", :development),
            Book.new("Designing with Web Standards", "Jeffrey Zeldman", :design),
            Book.new("Don't Make me Think", "Steve Krug", :usability),
            Book.new("JavaScript Patterns", "Stoyan Stefanov", :development),
            Book.new("Responsive Web Design", "Ethan Marcotte", :design)
        ]
        File.open "books.yml", "w" do |f|
            f.write YAML::dump lib_obj
        end
    end

    before :each do
        @lib = Library.new "books.yml"
    end

end

This is all set-up: we’re using two before blocks: one for :each and one for :all. In the before :all block, we create an array of books. Then we open the file “books.yml” (in “w”rite mode) and use YAML to dump the array into the file.

Short rabbit-trail to explain YAML a bit better: YAML is, according to the site “a human friendly data serialization standard for all programming languages.” It’s like a text-based database, kinda like JSON. We’re importing YAML in our spec_helper.rb. The YAML module has two main methods you’ll use: dump, which outputs the serialized data as a string. Then, load takes the data string and coverts it back to Ruby objects.

So, we’ve created this file with some data. Before :each test, we’re going to create a Library object, passing it the name of the YAML file. Now let’s see the tests:

describe "#new" do

    context "with no parameters" do
        it "has no books" do
            lib = Library.new
            lib.should have(0).books
        end
    end
    context "with a yaml file parameter" do
        it "has five books" do
            @lib.should have(5).books
        end
    end
end

it "returns all the books in a given category" do
    @lib.get_books_in_category(:development).length.should == 2
end

it "accepts new books" do
    @lib.add_book( Book.new("Designing for the Web", "Mark Boulton", :design) )
    @lib.get_book("Designing for the Web").should be_an_instance_of Book
end

it "saves the library" do
    books = @lib.books.map { |book| book.title }
    @lib.save
    lib2 = Library.new "books.yml"
    books2 = lib2.books.map { |book| book.title }
    books.should eql books2
end

We start with an inner describe block especially for the Library#new method. We’re introducing another block here: context This allows us to specify a context for tests inside it, or spec out different outcomes for diffenent situations. In our example, we have two different context: “with no parameters” and “with a yaml file parameter”; these show the two behaviours for using Library#new.

Also, notice the test matchers we’re using in these two tests: lib.should have(0).books and @lib.should have(5).books. The other way to write this would be lib.books.length.should == 5, but this isn’t as readable. However, it shows that we need to have a books property that is an array of the books we have.

Then, we have three other tests to test the functionality of getting books by category, adding a book to the library, and saving the library. These are all failing, so let’s write the class now.

class Library
    attr_accessor :books

    def initialize lib_file = false
        @lib_file = lib_file
        @books = @lib_file ? YAML::load(File.read(@lib_file)) : []
    end

    def get_books_in_category category
        @books.select do |book|
            book.category == category
        end
    end

    def add_book book
        @books.push book
    end

    def get_book title
        @books.select do |book|
            book.title == title
        end.first
    end

    def save lib_file = false
        @lib_file = lib_file || @lib_file || "library.yml"
        File.open @lib_file, "w" do |f|
            f.write YAML::dump @books
        end
    end
end

We could write up more tests and add a lot of other functionality to this Library class, but we’ll stop there. Now running rspec spec, you’ll see that all the tests pass.

This doesn’t give us that much information about the tests, though. If you want to see more, use the nested format parameter: rspec spec --format nested. You’ll see this:


A Few Last Matchers

Before we wrap up, let me show you a couple of other matchers

  • obj.should be_true, obj.should be_false, obj.should be_nil, obj.should be_empty - the first three of these could be done by == true, etc. be_empty will be true if obj.empty? is true.
  • obj.should exist - does this object even exist yet?
  • obj.should have_at_most(n).items, object.should have_at_least(n).items - like have, but will pass if there are more or fewer than n items, respectively.
  • obj.should include(a[,b,...]) - are one or more items in an array?
  • obj.should match(string_or_regex) - does the object match the string or regex?
  • obj.should raise_exception(error) - does this method raise an error when called?
  • obj.should respond_to(method_name) - does this object have this method? Can take more than one method name, in either strings or symbols.

Want to Learn More?

Rspec is one of the best frameworks for testing in Ruby, and there’s a ton you can do with it. To learn more, check out the Rspec website. There’s also the The Rspec book, which teaches more than just Rspec: it’s all about TDD and BDD in Ruby. I’m reading it now, and it is extremely thorough and in-depth.

Well, that’s all for this lesson! Next time, we’ll look at how we can use Rspec to test the interfaces in a web app.

Advertisement