The RSpec Ruby gem is one of the best testing libraries for testing Ruby code. Testing your code is very important: in fact, tests are essential in setting the parameters of what your program is all about. Imagine tests as the guideposts and scaffolding of your code. We write tests as we build out our code to ensure our models align with the goals of our program.
This post takes a leisurely stroll through the process of testing your code with RSpec.
Setting Up RSpec
The first step in test-driven development (TDD) is to configure RSpec as a dependency via Bundler. Create a new directory in your project and title this one “Gemfile.” Within it type the following code.
# Gemfile source "https://rubygems.org" gem "rspec" gem "pry"
Open this project’s directory in your terminal and type bundle install –path .bundle. This installs the latest version of RSpec and all related dependencies. You’ll see something that resembles the following:
Perfect! You have now installed your test suite with the RSpec and Pry gems. Both gems are necessary for helping us build out our project.
Now we are ready to build a small project. This project will have two classes: a Song and Library class. Instances of the Song class (each new song object) contain attributes: a name, an artist, and a genre — just as every song belongs to an artist and a genre. Our Library Class stores a library of music — of song objects — can save these songs to a file and allow the user to query them by their name and genre.
This is what our project directory should look like:
In our program we write the specifications (the “specs”) in a spec directory. For each class file we have a corresponding spec_helper.rb file. For our specs to run we need to “require” the associative Ruby classes we are testing. We declare this in the spec_helper file like so:
require_relative "../itunes_library.rb" require_relative "../song"
Think of require_relative as “require”: here, instead of scanning your Ruby path it searches for the relative path to your current directory.
For the purpose of this example we will be requiring YAML. YAML is a very basic text database we will use to store our program data.
The Song Class
Now we can start building out our Song class. Remember to require “spec_helper” at the top of our file to see what’s going on under the hood of our method.
require "spec_helper" describe Song do end
We start our spec_song.rb with a describe block which introduces what we are testing: this could be a string, but in this example we are using the class Song name.
before :each do @song = Song.new "Name", "Artist", :genre end
We begin by calling “before”, passing the symbol iterator :each to declare that we want to run this code before each test. And what are we doing before each test? We are generating new instances of Song according to attributes “Name”, “Artist” and “:genre”. Note the syntax of these attributes: whereas name and artist are strings :genre references something different.
When we write specs we reference instance variable names with the @symbol. We do this to ensure our instance variable is accessible across our tests; if we don’t use “@” we can only access this local variable within each individual code block (between the before and end of this code block).
Now we can create our first test which describes the actions of a specific method we are testing. As we write out our first test we are testing to see if our initialize method is making a Song object.
describe "#initialize" do it "takes three attributes and returns a Song object" do expect(@song).to be_a(Song) end end
It’s convention in test-driven development to include a “describe” block for our test. We use the hashtag symbol “#” to refer to instance methods like this: ClassName#methodName. We write the class name in the top level of our block to describe and include the method name which we want to reference.
Take a look at the syntax in the above example. We use expect(object).to do_something. This form will account for 9.9 out of 10 tests you write: you have an object, you expect it do something, then you pass that object the call of another function. This reads well and makes for a fairly intuitive test. Above, as we are testing the instance of @song, we reference this variable with the @ symbol.
Let’s run this. In TTD we run our tests in our terminal with rspec spec. Remember: the spec is the directory we created to hold our method tests.
Here’s a problem: when we run this program we encounter an error: “uninitialized constant Object::Song.” This means there is no Song class! Let’s add this now.
In TDD we only write enough code to fix the problem at hand. It is important in RSpec to write the smallest possible test case matching what we need to program in our Class.rb file (Andrew Burgess, “Ruby For Newbies“).
Moving from our spec/song.rb to our song.rb file, we write our code for the Song class:
class Song end
Re-run our tests and you see we are failing them. Here’s why: we are lacking an initialize method with three arguments (to initialize a new instance of song for our song class) so calling #initialize has no effect on our program. Typically, we follow the process of writing a test (or tests), testing these tests, seeing these tests fail, making these tests pass, refactoring our code, and repeating this process until we are done.
Onto more tests for our Song:
describe "#title" do it "returns the correct song title" do expect(@song.title).to eq("Title") end end describe "#artist" do it "returns the correct artist" do expect(@song.artist).to eq("Artist") end end describe "#genre" do it "returns the correct genre" do expect(@song.genre).to eq(:genre) end end end
This logic is straightforward: as you read the above tests you should know exactly what they are doing and referencing.
But run your tests again and you see they are failing! This is because in our Song class we have to initialize new instances of Song with the attributes outlined above: :title, :artist, and :genre. Let’s fix this now in our Song.rb file.
class Song attr_accessor: :title, :artist, :genre def initialize(title, artist, genre) @title = title @artist = artist @genre = genre end end
We pass in three arguments to our Song class which initializes new instances of Song with a title, an artist, and a category. Run our tests now and they pass.
You may notice although our tests pass we are not able to track our progress within our test suite. Luckily we can fix this by customizing the way RSpec runs by creating a “.rspec” file in our main directory.
RSpecing the Library Class
This is slightly more complicated and involves some hard coding. Let’s build out a new instances of song in our Library object.
require "spec_helper" require "pry"
describe "Library" do before :all do lib_obj = [ Song.new("The End", "The Doors", :classic_rock), Song.new("Oops I did it Again", "Britney Spears", :pop), Song.new("Blackstar", "David Bowie", :glam_rock), Song.new("Save Me", "Queen", :glam_rock), Song.new("Strange (I've Seen That Face Before)", "Grace Jones", :grace_jones) ] File.open "songs.yml", "w" do |f| f.write YAML::dump lib_obj end # This dumps the library object into new YML file. end
before :each do @lib = Library.new "songs.yml" end end
We use two :before blocks: one for :each and one for :all. In the before :all code block we hard code some of our data to use in our tests. Here I have created an array of Songs, each with a corresponding genre, title, and artist. When we run our program we will open the file “songs.yml” in “w” write mode and use YAML to dump this data as an array into this file.
You’ll notice when we run rspec spec in our terminal, YAML zips up this data and dumps it into a new “songs.yml” file in our main directory. Check it out:
YAML is “is a human friendly data serialization standard for all programming languages.” Essentially, a text-based database kind of like JSON. We import our YAML in our spec_helper.rb by requiring it with our other gems.
In our example we use two commands: dump, which spits out serialized data into a string, and load which takes the data string and converts it back into Ruby objects. The YAML load syntax looks something like this:
def initialize(lib_file = false) @lib_file = lib_file @songs = @lib_file ? YAML::load(File.read(@lib_file)) :  # YAML syntax to initialize text file with song data. end
So far we have created a file with some data we’ve hard coded. Perfect. Before :each test we create a new Library object, passing it the YAML file name. We write our corresponding tests for this:
describe "#initialize" do context "initialize Library with no parameters" do it "Library has no songs" do lib = Library.new expect(lib.songs).to eq() end end context "Initialize Library with a YAML file parameter" do it "Library has five songs" do @lib.songs.length == 5 end end end describe "#get_songs_in_genre" do it "returns all the songs in a given genre" do @lib.get_songs_in_genre(:classic_rock).length == 2 end end describe "#add_song" do it "accepts new songs and adds to library" do @lib.add_song(Song.new("A Jealous Heart Can Never Rest", "The Black Madonna", :techno)) end end end
We start these tests with a describe block for the #initializeLibrary method. Notice above new code block: context, which specifies a particular testing situation for our #initialize method. Here we spell out two contexts: “Initialize Library with no parameters” and “Initialize Library with a YAML file parameter.” In the former we are testing a Library object at initialization without any data. What does this look like? Well, an empty array of course! Thus we write:
When we initialize with a YAML file we expect .length of Library to equal 5 because we hard coded 5 songs in our lib_obj. Run these tests and you’ll see they are passing for this particular texting context.
Naturally, we’d like to build out the functionality of our program. Let’s say we want our program to add a new instance of Song to our Library object — or query a song by its title. Let’s write these methods in our Library.rb file:
class Library attr_accessor :songs
def initialize(lib_file = false) @lib_file = lib_file @songs = @lib_file ? YAML::load(File.read(@lib_file)) :  # YAML syntax to initialize text file with song data. end def get_songs_in_genre(genre) @songs.select do |song| song.genre == genre end end def add_song(song) @songs.push song end def get_song(title) @songs.select do |song| song.title == title end.first end [...]
Once we are happy with the functionality of our program we stop. Of course we can continue writing methods and their tests ad infinitum — until our program does what we want it to do.
Let’s stop here and run our tests.
RSpec is sometimes described as a Domain-Specific Language (DSL) because it describes the behaviour of objects. As programmers we write our tests first because we need to know what to test before learning how to test (Build Awesome Command Line Applications in Ruby: 143).
Moral of the story: learn to love RSpec.
Fork my repo here.