R³ Series #1: Getting started with Ruby on Rails and RSpec
Web development is “in”. The web allows a developer to quickly develop an application and make it available to thousands of users. Web applications can be written in a lot of different languages.
This article series focus on writing web applications with Ruby and Ruby on Rails. We will cover test-first development using RSpec a behavior-driven testing framework for Ruby. Future articles may introduce additional topics such as Continuous Integration, Continuous Deployment and Automatic Deployments as well as design patterns like decorators or presenters.
There will be little to no introduction into Ruby but I will try to hold it as easy as possible. I also expect little experience with web development and programming in general. I am using Linux for development and I will use command line tools without telling you how to open a command line. Mac OS X users should be able to enter most commands without problems. Windows users are recommended to use a virtual machine with Linux or installing a distribution for better performance ;).
Getting Started
Today we start building an assignment management system I call Shufflespace, where students can upload their solutions for regular course assignments and review their overall progress. There may be additional features like notifications about comments or grades, allowing students to work as correctors or using external authentication systems.
We will start with a very basic subset of these requirements and will add some new features or refactor existing code in each article. If there is some certain topic you want to be covered or a thing you do not get right please leave a comment and I will try to integrate it in the series.
So let’s start. First we need to install Rails and create a new Rails application. Open a command prompt and enter the following to install Rails:
~$ gem install rails
This will install Rails using RubyGems, a packaging and distribution channel for ruby programs and libraries.
Now we can create a new Rails application using the rails
command.
~$ rails new shufflespace -T
This command creates a new directory shufflespace
that contains our new basic Rails application. The -T
argument tells rails to not create a test directory for Test::Unit because we want to use RSpec instead of Test::Unit.
The rails command will create a lot of files and directories. The app
will contain our controllers, models and views. Configuration files and start-up related files can be found in config
. The lib
directory consists of more generic reusable application components. In public
reside static web-accessible files similar to a htdocs
directory from web servers. Files in public
can be directly opened in a web browser when the rails server is running.
After the files were created, the rails
command has automatically ran bundle install
. This will install all dependencies defined in Gemfile
. This will be the place where we can add our own dependency when using third party libraries.
Adding RSpec
Because we know that tests are important to achieve qualitative software we start by adding our test framework RSpec.
Take a look at your Gemfile
:
source 'https://rubygems.org'
gem 'rails', '3.2.8'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
gem 'sqlite3'
# Gems used only for assets and not required
# in production environments by default.
group :assets do
gem 'sass-rails', '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', :platforms => :ruby
gem 'uglifier', '>= 1.0.3'
end
gem 'jquery-rails'
# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'
# To use Jbuilder templates for JSON
# gem 'jbuilder'
# Use unicorn as the app server
# gem 'unicorn'
# Deploy with Capistrano
# gem 'capistrano'
# To use debugger
# gem 'debugger'
The first line tells Bundler, the tool responsible for installing all dependencies, where it should look for it. RubyGems.org is the current state-of-the-art source for gems. Nearly all available Ruby libraries can be found there.
To add RSpec as a dependency add the following code to your Gemfile
:
group :development, :test do
gem 'rspec-rails'
end
The gem rspec-rails
is an adapter that provides a better integration of RSpec and Rails. Because it has rspec
as a dependency we do not need to add rspec
ourself.
The group
command tells bundler
to only install rspec-rails
if the development or test environments are not excluded from install. That usually is the case when deploying on a server where no development tools or test frameworks are needed.
After we have added RSpec we have to run the bundle install
command to install all new dependencies. You will notice that bundler uses the existing, already installed gems as well as installing the new required ones:
...
Using railties (3.2.8)
Using coffee-rails (3.2.2)
Installing diff-lcs (1.1.3)
Using jquery-rails (2.1.3)
Using rails (3.2.8)
Installing rspec-core (2.11.1)
Installing rspec-expectations (2.11.3)
Installing rspec-mocks (2.11.3)
Installing rspec (2.11.0)
Installing rspec-rails (2.11.4)
...
Now we have successfully installed RSpec. Let’s go on with adding a basic user model and a controller and views to list, create and edit users. After installing RSpec we need to run rails generate rspec:install
to install RSpec’s base directory and creating the necessary files to run RSpec tests.
Now would be a great moment to commit everything in a git repository to be able to revert if anything goes wrong later.
Scaffolding A User
Rails provides a command line tool to generate most parts of an application. Execute rails generate
to get an overview of available generators.
We start with the scaffold
generator. The scaffold
generator generates a database migration, a model, a corresponding controller with all RESTful actions and all necessary views.
~/shufflespace$ rails generate scaffold User login:string \
first_name:string last_name:string email:string admin:boolean
This creates a user model with attributes login
, first_name
, last_name
, email
and admin
. All attributes except admin
will be stored as strings. Let’s take a look on what is generated:
invoke active_record
create db/migrate/20121104110138_create_users.rb
First the scaffold generator created a migration. A migration describes steps to get a specific database layout. Instead of most other web frameworks Rails does not define attributes within the model class but reads the attributes from the database scheme. We use migrations to develop the database.
Migrations create tables, add or remove columns or define indexes to optimize performance.
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
After the migration Rails has generated the model class in app/models/user.rb
and the matching test file for RSpec. RSpec test files (*specs*) are usually named the same as the file they test suffixed with _spec
. Let’s take a look at the model file:
class User < ActiveRecord::Base
attr_accessible :admin, :email, :first_name, :last_name, :login
end
The user model inherits from ActiveRecord::Base
, the base class for models using Rails' database mapper ActiveRecord. There is no table name defined in the model. That is because Rails favors convention over configuration (COC). By naming our model User
Rails assume there is a table called users
and will automatically store users there as well as using the columns of users
as our model attributes. If we gaze at the generated migration file we will find a matching table definition:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :login
t.string :first_name
t.string :last_name
t.string :email
t.boolean :admin
t.timestamps
end
end
end
You might miss the definition of a primary key called id
. That is also covered by Rails' COC. Rails automatically creates a primary key id
that will be auto-incremented for each table (unless declared otherwise). Similar the line t.timestamps
creates two columns named created_at
and updated_at
. They will contain the date a record was created and last updated. Rails will automatically set or update these fields without any manual configuration.
If we further look what files where generated we see a:
invoke resource_route
route resources :users
Here Rails added a line to config/routes.rb
. That files defines how URLs are mapped to controller actions. If we take a look at out config/routes.rb
we will see our users:
Shufflespace::Application.routes.draw do
resources :users
...
end
Actually resources :users
is a helper that generates seven URL mappings. Five basic REST routes and two routes for display nice forms. I can also write the above like this:
# five basic REST routes
get '/users' => 'users#index', as: :users
post '/users' => 'users#create'
get '/users/:id' => 'users#show', as: :user
delete '/users/:id' => 'users#destroy'
put '/users/:id' => 'users#update'
# two nice form routes
get '/users/new' => 'users#new', as: :new_user
get '/users/:id/edit' => 'users#edit', as: :edit_user
The method name defines the HTTP method the request must have to match a route. The first string defines the path that must be matched. :id
is a so called path parameter. The second string describes the controller and action that should be called.
When a GET request on /users/5
comes in it will match the rule get '/users/:id' => 'users"show', as: :user
and will be routed to the UsersController’s show action with the parameter id
as 5
.
Rails' scaffold
generator has further generated the controller and views and the controller and view specs:
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke erb
create app/views/users
create app/views/users/index.html.erb
create app/views/users/edit.html.erb
create app/views/users/show.html.erb
create app/views/users/new.html.erb
create app/views/users/_form.html.erb
invoke rspec
create spec/controllers/users_controller_spec.rb
create spec/views/users/edit.html.erb_spec.rb
create spec/views/users/index.html.erb_spec.rb
create spec/views/users/new.html.erb_spec.rb
create spec/views/users/show.html.erb_spec.rb
create spec/routing/users_routing_spec.rb
invoke rspec
create spec/requests/users_spec.rb
By default (Rails' COC) the controller UsersController is name like the pluralized resource User it serves. The views are named like the controller action. So for UsersController#show
the file app/views/users/show.html.erb
will be rendered.
Afterwards Rails created a helper module for utility methods required in user views and again a spec file for our helpers.
invoke helper
create app/helpers/users_helper.rb
invoke rspec
create spec/helpers/users_helper_spec.rb
Last but not least Rails generated some asset files for styles and JavaScript (or SCSS and CoffeeScript to be exactly).
invoke assets
invoke coffee
create app/assets/javascripts/users.js.coffee
invoke scss
create app/assets/stylesheets/users.css.scss
invoke scss
create app/assets/stylesheets/scaffolds.css.scss
With all these generated files we already have a fully working rails application. Let’s try to see something in a browser.
Running Our Application
First we need to update our database because we have added a new migration. Our application uses a SQLite3 database so we just need to run all migrations to create and setup our database:
~/shufflespace$ rake db:migrate
== CreateUsers: migrating ====================================================
-- create_table(:users)
-> 0.0015s
== CreateUsers: migrated (0.0222s) ===========================================
Now we can launch a web server by simply typing rails server
.
~/shufflespace$ rails server
=> Booting WEBrick
=> Rails 3.2.8 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2012-11-04 13:11:33] INFO WEBrick 1.3.1
[2012-11-04 13:11:33] INFO ruby 1.9.3 (2012-10-12) [x86_64-linux]
[2012-11-04 13:11:33] INFO WEBrick::HTTPServer#start: pid=16710 port=3000
This will fire up WebRick a simple web server written in Ruby locally on port 3000. Now open a browser and surf to http://localhost:3000/users. You should see an empty list of users.
Try adding a new user, edit an existing one and delete user you do not want anymore. You already have a fully working application.
Adding Validations
While playing around you may have seen that you can enter every value you want. There is no validation if a login is already taken or if you entered a valid email.
So lets start with adding a validation to ensure each user has a unique login. Because we will develop test-first we first add a test to our model spec:
require 'spec_helper'
describe User do
pending "add some examples to (or delete) #{__FILE__}"
end
The describe
block defines what we are testing and automatically choose the right environment when running the test. pending
prints a warning when running the test but does not make them fail. All auto-generated tests contains a pending
warning so it is easy to find empty tests when running the whole test suite.
We want to write a test to check that if a user has an already taken login he cannot be saved.
require 'spec_helper'
describe User do
let(:user) { User.create!(login: 'jsmith') }
before { user }
it "should have a unique login" do
another_user = User.new login: 'jsmith'
another_user.login.should == user.login
another_user.should_not be_valid
end
end
This spec is really basic. We are not using any kind of fixture or factory and we are not even testing what is the reason the user is invalid. Anyhow it shows the basic structure of an RSpec spec. We will use this spec for refactoring in a later article when introducing Factories and Shoulda. But first lets talk about what we have written here.
let(:user) { User.create!(login: 'jsmith') }
This line allows us to use user
in each test. RSpec assures that each test (each it
) gets its own user, but within a test always the same object will be returned. We can modify user
within a test like any local variable but in the next spec it will all be reset. We are using user
to validate we have two users with the same login in line 9.
before { user }
Any code in a before block will be executed before each test. This allows us to setup some database records like an already existing user. We are using user
here to avoid code duplication with the let
command. So before each test a user with the login jsmith
exists in our database.
it "should have a unique login" do
Here we are starting our first test. We give a short description what the test does because RSpec will show us these description when running the tests as well as when generating some kind of documentation based on our tests.
Now lets create a new user with a non unique login:
another_user = User.new login: 'jsmith'
This will build but not save the user because saving would fail.
Last but not least we check that both existing users have same login and the new user is not valid.
another_user.login.should == user.login
another_user.should_not be_valid
Before we start to implement the validation first ensure our tests are failing. Run the following command to execute all tests:
~/shufflespace$ rake spec
ruby -S rspec ./spec/models/user_spec.rb ./spec/views/users/edit.html.erb_spec.rb ./spec/views/users/index.html.erb_spec.rb ./spec/views/users/new.html.erb_spec.rb ./spec/views/users/show.html.erb_spec.rb ./spec/routing/users_routing_spec.rb ./spec/requests/users_spec.rb ./spec/helpers/users_helper_spec.rb ./spec/controllers/users_controller_spec.rb
.F.*..........................
Pending:
UsersHelper add some examples to (or delete) ./spec/helpers/users_helper_spec.rb
# No reason given
# ./spec/helpers/users_helper_spec.rb:14
Failures:
1) User should have a unique login
Failure/Error: another_user.should_not be_valid
expected valid? to return false, got true
# ./spec/models/user_spec.rb:10:in `block (2 levels) in <top (required)>'
Finished in 0.49721 seconds
30 examples, 1 failure, 1 pending
Failed examples:
rspec ./spec/models/user_spec.rb:7 # User should have a unique login
Randomized with seed 53212
As you can see we have on pending spec UsersHelpers
. This spec is pending because we do not have added any tests yet. We also have one failure in user_spec.rb
. It tells us that another_user
should not be valid but was. That is actually true. Because we do not have added any validation yet the new user really is valid. So lets add enough code to pass our test.
We need to define a uniqueness validation within our User
model so add the following to app/models/user.rb
:
class User < ActiveRecord::Base
attr_accessible :admin, :email, :first_name, :last_name, :login
validates :login, uniqueness: true
end
The added line adds a uniqueness validation on login
. Now every user with a non-unique login cannot be saved anymore. If you rerun rake spec
you will get no failing specs anymore and if you try to add a new user with an existing login you will get a nice error message:
That finish our first test-driven added feature and also this first introduction.
What We Have Done
Lets summarize what we have done until now.
We first installed Rails and created a new application. We added RSpec and created a scaffold, a complete set of migration, model, controllers and views. We had a fully working application since this moment. Later we wrote a test for a uniqueness validation on the user model, ensured it failed and then implemented our new feature.
That finalizes our first impression on Rails. We have created a running and working application without writing more than ten lines of code. And that even includes our tests to ensure everything works right. Impressive, right?
In the next article I plan to cover one of the following topics:
- Associations: We can add a new model, e.g.
courses
, and add an relation between users and courses. - Factories: We can add a factory for creating objects in our specs to easy extend our models without changing every test.
- Views: We can take a deeper look at Rails views, how they are created, how they are related to helpers and what alternative formats for views exists.
Just leave a note about what you would like to hear. That also will lift the huge responsibility of choosing a topic from my shoulders ;). Also do not hesitate to post questions or criticism, I am new to writing tutorials so there may be much space for improvements. Thank you.
Update: You can access all source code on GitHub.
Useful resources:
- http://guides.rubyonrails.org Ruby on Rails guides
is licensed under Creative Commons BY-NC-SA 3.0.