In Part II of this Ruby on Rails tutorial, I’m going to hit the ground running with the concept of Behavior-Driven Development, or BDD. We’ll get into RSpec, its syntax, and why we use it, all while creating ourselves an `author’ resource that will form the basis of our blog system. I’ll be assuming that you’ve got Rails installed, an application started, and your database set up (though, currently, table-less), since Part I covers how to do those. Before starting to work on this, you’ll want to fire up the database server so that we can use it.
Ruby’s a nice language. People who really like it will even tend towards saying it’s the best language to do anything you could ever possibly want to do with any language ever. That’s not necessarily true, but the point is, Ruby is nice. And it’s easy to extend. It’s easy to write what we call `Domain Specific Languages’ in Ruby. DSLs, as they’re commonly abbreviated, are just programming languages that are custom-tailored to a specific task, so that they represent a vocabulary and usually a syntax particularly well-suited for that task. SQL, for example, can be called a domain specific language for database queries.
So in the general vein of DSLs, we get to RSpec. RSpec can be called a Domain Specific Language for Behavior-Driven Development. Uh-oh. Another acronym! (That’s BDD, mind you.) So, before talking any more about RSpec, we need to talk about what BDD is.
BDD: Behavior-Driven Development
There are many good resources on BDD; the RSpec website , for example, lists some good ones, and provides a basic example right up front. BDD is all about letting expected behavior drive your programming. It’s also about developing incrementally, first specifying the actual behavior you want, then implementing the code that actually makes it happen. This typically results in software that’s well-tested—that is to say, that has checks to ensure that it does what it should. This software is, then, more maintainable, because if you want to change the way one thing works somewhere, and it breaks other things, you should know instantly when your behavior specifications fail to work appropriately.
So the base unit of BDD is a specification: a detailing of how something should behave. Typically, we’ll group specifications into contexts—basically nothing more than a grouping of specifications. Now, these are abstract concepts, so it can be difficult to understand how exactly they operate. I’ll be showing by example here, though, so that should clarify things. Moreover, the syntax of RSpec should itself help to clarify what’s doing what and how things fit together. That’s just a side-effect of some of the excellent language choices made by the RSpec folks.
RSpec and Rails
RSpec is a standalone BDD testing framework, but it also has a plugin for Rails devoted to simplifying Rails testing. This plugin is called Spec::Rails. So the first thing we’ll have to do for blogification is to install the plugin. Let’s get started right now and head on over to our application’s root directory:
> cd blogification
Now we’ll install the plugin. For this, we use the plugin script1:
> ruby script/plugin install "svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec"
... Some output ...
> ruby script/plugin install "svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails"
... Some output ...
The first bit installs RSpec directly as a plugin for our Rails application2. The second bit installs Spec::Rails, or RSpec on Rails as it’s sometimes called. These only exist for your Rails application, and they live in your Rails application directory, which means bundling your Rails application still involves just bundling up that one directory3.
As a one-time thing, we also need to set up some RSpec folders and files, so we run:
> ruby script/generate rspec
That’ll create some files rspec needs to be all it can be. So now we’ve got RSpec installed, and it’s time to start using it!
Authors
Since we’re making a blog system, it follows that there are going to be some authors involved, right? Somebody’s going to have to write those blog posts! So, initially speaking, let’s just give these authors a username and a display name, which will be the name that shows up on the actual posts. Let’s write a quick little `specification’—a description of how authors in general should behave:
An Author:- should always have a username
- should have a username longer than 6 characters
So we’re just going to require that the author have a username and that it be longer than 6 characters. What that means for our application is that we should never be able to create an author without a username or with a username shorter than 6 characters.
Now that we’ve got an idea of what we’re going to create, it’s time to create some Rails files! It turns out, we’ll initially just let Rails do the magical file generation for us. We hit up the main folder of our application, and run:
> ruby script/generate rspec_model author
We get some nice shiny output, basically telling us all the files Rails created for us. The important files we’ve just had created are:
db/migrate/001_create_authors.rb—what we call a `migration’; basically, a Ruby file that describes some changes we’re going to make to our database to support this new model.app/models/author.rb—the file where we’ll be programming what the author will do. We call this, conveniently enough, a `model’. If you remember the bit about MVC in the first part, the model keeps track of the data. Basically, this is the part of our application which will interact with the database.spec/models/author_spec.rb—the file where we write our specifications. This is where we’ll programatically describe the above requirements.spec/fixtures/authors.yml—the file where we’ll be putting test data. We can use this to write specifications for `existing’ models without having to manually create them every time. We won’t necessarily be using those right now, but maybe later.
The first place we go is spec/models/author_spec.rb. In Behavior-Driven Development, the first thing we want to do is write our specifications—we want to describe what our code is supposed to do. We only write the code itself afterwards. In our case, we’ll try to codify the two requirements we put above in our spec.
Spec’ing the Author
When you first open spec/models/author_spec.rb you’ll see something like this:
require File.dirname(__FILE__) + '/../spec_helper'
describe Author do
before(:each) do
@author = Author.new
end
it "should be valid" do
@author.should be_valid
end
end
Ok, so that looks like a dummy test, basically. We require spec_helper.rb, which contains common functionality for all specs and some configuration (go ahead and open it up if you’re interested). Then we have the RSpec specification. First off, we see this line:
describe Author do
The do/end pair is the block that will enclose everything we’ll be describing. We’re saying that an author, in general, should be described by the ensuing specifications. This is the grouping we call a `context’. Then, we see this bit:
before(:each) do
@author = Author.new
end
Just like the code says, this is a bit of code that will run before each specification. So we will have a new, clean author before each specification runs. Moreover, notice that we assign to an instance variable. This is because all the code run in the describe block is being run inside a class instance, so we can use instance variables to refer to a variable in the before(:each) block and in the specifications later on.
Finally, there’s this:
it 'should be valid' do
@author.should be_valid
end
There it is! That’s a specification. The string we pass to the it method describes what we’re checking. You can read this bit, altogether, as `an author, in general, should be valid’. The line inside the block just checks that the author is valid.
This specification in and of itself isn’t too useful to us, but it gives us a little something about RSpec syntax. Now we’ll write our specs. First, we want to specify that an author requires a username. What does that mean? That means that, if the author is created without a username, they should be marked as invalid, and we should have an error message associated with the username field. That’s just the way we want it to work. Basically, it’s impossible to save an author to the database if they don’t have a username (because invalid model objects can’t be saved to the database), and, when such an event happens, we want to have an error message associated with it so that we can let the user know what happened. Great! Let’s write the specification. First, delete the existing describe block, and let’s replace it with a new one:
describe Author, 'without a username' do
before(:each) do
@author = Author.new(:username =>nil)
end
it 'should not be valid' do
@author.should_not be_valid
end
it 'should have an error message associated with the username' do
@author.errors.on(:username).should_not be_nil
end
end
Ok, so we check here that an author without a username shouldn’t be valid and should have an error message associated with their username. Before each specification, we create a new author with a nil username. How’s that? The constructor for Rails models can take a hash of attributes and values. This basically initializes those attributes of the model to the specified values. A nil username is basically the same as no username, so that’s how we ensure that this new author will have no username.
Then, we check that it isn’t valid. We invert the previous specification by saying it should_not be valid. should and should_not form the basis of the RSpec tests. They can be thought of as the same as assert and assert !<condition> in Test::Unit. We also check that there’s an error message there by checking that the error on that attribute isn’t nil. Here, we use another little bit of Rails goodness: all models have an errors collection defined on them. This collection lets us get to errors associated with various attributes the model has. The collection itself provides many useful methods, but here we use the on method to get the error associated with the username attribute. Another way to write this would have been:
@author.errors[:username].should_not be_nil
This is because the errors collection is also built to behave like a hash. If you want to read up some more on this collection, you can check out the ActiveRecord::Errors documentation. Don’t worry, though, this tutorial will also offer more than just what we’ve seen so far.
Running the Spec
So now we’ve got our spec! And we want to write code! But we haven’t checked whether it passes yet… So let’s run our spec, just to test it. We’ll do it the quick ‘n dirty way. But first, let’s play with the output format of RSpec. Why? Because it’ll look cooler this way ;-) Open up spec/spec.opts and change the line that reads `progress’ to `specdoc’. That’ll change the output format. Now, go to the main blogification directory and run4:
> rake spec
Your output should look something like this:
Author without a username
- should have an error message associated with the username (ERROR - 1)
- should not be valid (ERROR - 2)
1)
ActiveRecord::StatementInvalid in 'Author without a username should have an error
message associated with the username'
Mysql::Error: #42S02Table 'blogification_test.authors' doesn't exist: SHOW FIELDS FROM authors
./spec/models/author_spec.rb:5:in `new'
./spec/models/author_spec.rb:5:
2)
ActiveRecord::StatementInvalid in 'Author without a username should not be valid'
Mysql::Error: #42S02Table 'blogification_test.authors' doesn't exist: SHOW FIELDS FROM authors
./spec/models/author_spec.rb:5:in `new'
./spec/models/author_spec.rb:5:
Finished in 0.339 seconds
2 examples, 2 failures
Owie. Double failure. Let’s look at what’s wrong. Read that MySQL error: Table 'blogification_test.authors' doesn't exist. Oh! We don’t have a table yet! Ok, time to head on over to the migration, then.
Migration
Migrations, as we mentioned earlier, are the way we update our database to match what we want. Think of it as a versioning system for your database. Initially, we’ll just want some authors, but later, we may want to add something else. Migrations let us worry about each of these individually, and then `migrate’ our database to include all of them. This is less useful in development sometimes, when you’re maybe going step by step anyway; however, once you’re headed to production, deploying the seventy changes you’ve made to your database can just be a matter of updating your application and then running the seventy migrations. And if that isn’t shiny, I don’t know what is.
Now, migrations are written in Ruby, though you can also execute plain SQL. Initially, we’re just going to worry about running some basic ones, so you won’t really need to run SQL explicitly5. So let’s write our first one. We’re going to go ahead and add the display name and the username to all authors. So, let’s open up db/migrate/001_create_authors.rb. Here, we’ll see this code:
class CreateAuthors < ActiveRecord::Migration
def self.up
create_table :authors do |t|
end
end
def self.down
drop_table :authors
end
end
Shiny. What’s it mean? First of all, this is just a class like any other. Inheriting from ActiveRecord::Migrations lets us actually write a migration. Then we see two class methods: up and down. up is run when you migrate to this version (remember that 001 in front of the filename? This is version 1 of the database schema). down is what runs if and when you decide to migrate back away from this version. It’s basically what you use to undo the effects of the up method.
In this code, we have a skeleton for what we want to do set up. It creates the authors table, which will contain information for all authors, and down drops it. create_table is taking a block, and that block takes a parameter, which we’ve called t. What is that parameter? It’s the basis of all of our magic. It lets us add all the columns we need to the table. Add this inside the create_table block:
create_table :authors do |t|
t.column :username, :string, :null => false
t.column :display_name, :string
end
This will create for us a username column, which will be a string and will not be allowed to be null6, and a display name, which will also be a string, but will be allowed to be null. Also, behind the scenes, Rails creates an id column, which will be the primary key of this table. It’s set up to automatically increment and everything. This key is a surrogate key, and it bears mentioning that Rails uses surrogate keys for all tables by default (this is behavior that can be modified, but doing so can be annoying since so many things in Rails are based on the idea that everything uses surrogate keys).
Ok, so now we’ve got our migration. Yay! Let’s run our spec again:
> rake spec
Uh-oh… We got the same errors! What happened? We didn’t run the migration. Just writing a migration isn’t enough, you have to run it so that it makes the relevant modifications to the database. So, let’s migrate our database to the latest version we have:
> rake db:migrate
You get some output that tells you that the CreateAuthors migration ran, and that the create_table(:authors) bit worked. It’ll even tell you how long it took! Wonderful. Now, let’s run that spec again:
> rake spec
Let’s see that output:
Author without a username
- should have an error message associated with the username (FAILED - 1)
- should not be valid (FAILED - 2)
1)
'Author without a username should have an error message associated with the user
name' FAILED
expected not nil, got nil
./spec/models/author_spec.rb:13:
2)
'Author without a username should not be valid' FAILED
expected valid? to return false, got true
./spec/models/author_spec.rb:9:
Finished in 0.399 seconds
2 examples, 2 failures
Oh dear, two failures. Notice that when something fails or errors out, we get a number next to it, and later on the error/failure messages themselves are numbered. This lets us refer back and forth between them.
Now, let’s see… The problem happened because the author without a username was not invalid, and there was no error message associated with the username attribute. So that means we want to go make both those things happen. Notice that we didn’t write any extra code to be able to get to the username. It turns out, once you add a column to the database, your Author model automagically knows about it. So you have to do no extra work. The validation, on the other hand, does need a little more work.
Validations, and a Passing Spec
Okay, so let’s now head to the last component of the model: app/models/author.rb, the actual model class. Right now, it looks lonely:
class Author < ActiveRecord::Base
end
But that’s okay, let’s spice it up. We’re going to add a check for username not being blank!
class Author < ActiveRecord::Base
validates_presence_of :username
end
Bam! One line of code, and we have Rails checking to make sure there’s a username present for every author created or saved henceforth. Let’s run the spec!
Author without a username
Author Errors:
- should have an error message associated with the username (FAILED - 1)
- should not be valid
1)
'Author without a username should have an error message associated with the user
name' FAILED
expected not nil, got nil
./spec/models/author_spec.rb:14:
Finished in 0.352 seconds
2 examples, 1 failure
What happened there? It’s marked as invalid, but there’s no error; why’s that? It turns out, just calling @author.errors doesn’t actually check the model for being valid. And thus, it doesn’t necessarily load all the errors needed. Errors are only brought up when you check the validity of a model. `But,’ you might say, `we validated the model! The other spec checks whether the author is valid!’ Yes, but remember: all specs are run independently of each other. Before each spec, we create a completely new, un-validated author. So we have to validate it manually. Thus, we’ll change our error message specification to look like this:
it 'should have an error message associated with the username' do
@author.valid? # run validation checks
@author.errors.on(:username).should_not be_nil
end
The line that calls valid? will run the validation checks, though we ignore the return value (which we check to be false in the other specification). Notice that these two specifications check something similar, but we still make them separate specifications. This means we only test a very small amount of information in one specification. It’s also a good idea to keep contexts short. The context we’re working in, an author with no username, should only really have those two checks.
Now, let’s run that spec again:
Author without a username
- should have an error message associated with the username
- should not be valid
Finished in 0.338 seconds
2 examples, 0 failures
Look at that green! Our specs pass! Now we’re talkin’. And notice that the output looks a lot like the human idea spec we wrote at the very beginning of this part. So now that we’ve passed this spec, time to focus on the second requirement: that a username be longer than 6 characters in length.
Validating Length
So let’s write the spec:
describe Author, 'with a username shorter than 7 characters' do
before(:each) do
@author = Author.new(:username => 'test')
end
it 'should not be valid' do
@author.should_not be_valid
end
it 'should have an error message associated with the username' do
@author.valid? # run validation checks
@author.errors.on(:username).should_not be_nil
end
end
describe Author, 'with a username of 7 characters' do
it 'should be valid' do
@author = Author.new(:username => 'testers')
@author.should be_valid
end
end
Notice: we check both that a username shorter than 7 characters doesn’t work, and that a username at 7 characters does work. These are our boundary conditions, and it’s important to test either end of the check.
Run the spec now and satisfy yourself that it fails. The check that a username of 7 characters should work works fine—after all, currently the model doesn’t care how long the username is, as long as it’s there. Both of the checks for shorter than 7 characters messing up, however, fail. Time to add some more goodness to our model.
Add this line after the check for the presence of the username:
validates_length_of :username, :minimum => 7
That looks pretty natural: it checks that the username’s length is at least 7. So go ahead and run that spec now. Like that green? Yeah, it’s lovely.
Conclusion
Ok, that’s it for this part. We have a spec’ed out author model that requires a username that is longer than 6 characters long. What we’re missing is a way to create a modify these guys, and that’ll be the topic of the next installation of this tutorial. As a natural part of that, we’ll also look at how Spec::Rails handles testing controllers and views… And we’ll learn what controllers and views are, in case you haven’t guessed yet.
If you’re interested in finding out more about what kinds of validations are available out of the box with Rails, you can hit up the ActiveRecord::Validations::ClassMethods documentation in the Rails API.
1 Windows users—you’ll need to grab Subversion for this. You can get it on the Subversion download page.
2 RSpec can also be installed for standalone use using RubyGems. For this, just run gem install rspec.
3 For those of you using Subversion, running script/plugin install -x will add the plugin as an svn:externals entry so that you don’t have the files directly in your repository, but rather just a reference to the remote repository that will be fetched whenever you run an update. Another alternative to both of these approaches is Piston, which lets you manage the repository version independently of SVN’s internal management (so that you only update when you want to, not whenever you update the rest of your project from SVN).
4 Windows users—you may find yourself having to install the win32console gem. Just do a gem install win32console if you get a nasty error and stack trace.
5 I’ll take this opportunity to say right off the bat that Rails lets you get away with not using SQL in many places, but there’s a performance tradeoff involved in doing this. Once you go to production and can identify bottlenecks, you may have to bust out some of your mad SQL skillz to patch them up. Rails works fine with SQL, and in fact provides hooks to use straight SQL in many places. In short, Rails does not preclude SQL, nor does it consider SQL a bad idea, beyond the basic fact that premature optimization is often a bad idea in and of itself.
6 You might be thinking `ah-ha! Here’s where we’ve set it up so usernames can’t be blank!’ You would, however, be wrong. Database settings, beyond the type of the database column, aren’t really paid attention to by Rails. If you tried to save a username with a nil username, sure enough, you’d get a database error. But Rails wouldn’t understand that and give you an error message. Instead, we go through Rails validations to do that. Those operate before Rails ever hits the database. The situation is analogous with foreign key constraints and such. Rails adds its own layer to deal with such things, and generally ignores the database ones. In fact, foreign key constraints can be problematic with Rails, especially with Rails testing. An interesting post on this is at caboose .
6 Responses to “A Ruby on Rails Tutorial: Blogification, Part II: RSpec, BDD, and Authors”
Sorry, comments are closed for this article.
May 29th, 2007 at 08:23 PM
my god i love your work to me ,it’s so good i think it is a great espisode! test test test! i have to learn it from now…
May 30th, 2007 at 07:19 PM
Yeah, part of what I wanted to put through in this tutorial was BDD, and basically use it as a guiding principle throughout. So don’t worry, there’s plenty more coming :-)
May 30th, 2007 at 10:04 PM
hi,i came from china,hehe my english is poor will you using REST in this tutorial? thanks
May 31st, 2007 at 09:11 AM
Yep, definitely will be using REST. That’s what the next part is basically going to be about—how to turn the Author model into a full-on Author resource.
May 31st, 2007 at 09:16 PM
great,我期待下一篇,呵呵。
July 11th, 2007 at 12:04 AM
This is great! I’ve always smelled something quite un-Rails about Test::Unit