An Introduction to Outside-in Development
5/Oct 2010
This guest post is contributed by Harold Giménez, who is a web developer with the crew at thoughtbot, where he spends his days working with Rails and writing open source software. He’s also involved with thoughtbot’s workshops, which is a great way to dig deeper into subjects like test-driven Rails development. Follow Harold on twitter at @hgimenez.
An Introduction to Outside-in Development
Outside-in Development is a software development methodology that focuses on providing value to the product’s users and stakeholders. Cucumber is a BDD tool that supports Outside-in development by running plain text features or user stories as acceptance tests. Well written user stories state clearly their business value, and help developers bring themselves out of the software developer role and into the user role, where we can get a more realistic overview of the application’s interface and general flow. This leads to more cohesive and simpler software, because you write the simplest amount of code that satisifies a user story, and you often review the application as a user. Outside-in along with the test-driven process helps you write just the minimum amount of code that provides value to stakeholders, and not a line more.
While still great and recommended, reading much theory and praise about outside-in is not as effective as actually trying it and seeing it in action. We will write the beginnings of twiddr, a small twitter clone in Rails 3 using the outside-in process. Our testing arsenal will consist of the following excellent Ruby libraries.
- Cucumber: A plain text story runner.
- RSpec: A BDD framework for Ruby.
- Factory Girl: A fixture replacement framework.
- Shoulda: A set of RSpec matchers, among other things.
We will explain in more detail how these frameworks work together as we
start developing the application. To get started quickly, we will use
suspenders to create an
application with all of our tools installed and configured. Since this
is a Rails 3 application, we will use the beta version of suspenders.
Install it by running gem install suspenders --pre
. Now that
suspenders is installed, we can bootstrap twiddr with
suspenders create twiddr
.
At this point, we have a twiddr application including authentication
with clearance, a simple Rails
authentication solution with email and password. Clearance also includes
cucumber features that you can view in the features
directory. Let’s
run them right now by running rake db:migrate
to create the users
table, script/rails generate clearance_views
to generate clearance
views with formtastic, and
rake
to run all specs and features. You should see a very satisfying
series of green dots appear on your terminal, along with “0 Failures”.
Let’s create our first feature. The anatomy of a feature is the following:
In order to [business value]
As a [role]
I want to [some action]
Notice how a feature includes who, what and most importantly: why. Our first feature could look something like the following:
Feature: Users have a twiddr login name
In order to mention other twiddr users by login name
As a twiddr user
I can pick a login name
There will be many scenarios accompanying this feature. Scenarios will follow the four stages of testing: setup, exercise, verify and teardown. Let’s write a few scenarios that come to mind. A simple scenario looks like this:
Given [context]
When I do [action]
Then I should see [outcome]
The Given
step is where you set up the context of your scenario. Every
scenario starts with a blank slate, so it is important to create a state
in your application for example by creating data in the database, or by
navigating to a specific page. The When
step is where you exercise the
application in order to accomplish what needs testing. In the case of a
web app like twiddr, this is usually where you fill in forms, press
buttons, click links, or otherwise interact with the system in some way.
Finally, the Then
step is where you verify the result, and it’s where
we check that the correct pages are rendered, that we see a success or
error message, or anything that could help us verify that the prior
action was successful. As we move along with creating our own features,
this will become much clearer.
The final stage of testing, teardown
, is taken care of by cucumber
automatically. It refers to clearing out the state of the system so that
there are no nasty leftovers from one test to the next. In most cases,
this means deleting all data from the database.
Back to developing our app, we already have authentication set up in the
application via clearance, but there is value in also having a twiddr
login, so let’s go ahead and add that functionality to user model. To
get started, we can work off of the clearance sign up scenarios that you
can find in features/sign_up.feature
. Let’s modify the scenario titled
“User signs up with valid data” and add a step where the user fills in
their twiddr name. The step definition ends up looking like this:
Scenario: User signs up with valid data
When I go to the sign up page
And I fill in "Email" with "email@person.com"
And I fill in "Twiddr name" with "user"
And I fill in "Password" with "password"
And I fill in "Confirm password" with "password"
And I press "Sign up"
Then I should see "instructions for confirming"
And a confirmation message should be sent to "email@person.com"
The above is executable code. Cucumber uses regular expressions to match
the plain text features and executes the resulting step definitions.
Finally, note that each of these stages of a cucumber feature can have
many steps: by adding more steps starting with And
, we can create more
complex contexts (Given/And), actions (When/And) or outcomes (Then/And).
At this point it is worth viewing the step definitions provided by
Capybara if you haven’t already. They can be found in
features/step_definitions/web_steps.rb
, and have definitions for most
web interactions like I go to...
, I fill in...
, or I should see...
Let’s run the new scenario, and watch it fail:
\$ bundle exec cucumber features/sign_up.feature
The output will look something like this:
........F-----.................
(::) failed steps (::)
cannot fill in, no text field, text area or password field with id, name, or label 'Twiddr name' found (Capybara::ElementNotFound)
./features/step_definitions/web_steps.rb:41
./features/step_definitions/web_steps.rb:14:in `with_scope'
./features/step_definitions/web_steps.rb:40:in `/^(?:|I )fill in "([^"]*)" with "([^"]*)"(?: within "([^"]*)")?$/'
features/sign_up.feature:17:in `And I fill in "Twiddr name" with "user"'
Failing Scenarios:
cucumber features/sign_up.feature:14 # Scenario: User signs up with valid data
5 scenarios (1 failed, 4 passed)
31 steps (1 failed, 5 skipped, 25 passed)
0m0.852s
The error message tells us that the text field labeled “Twiddr name” was
not found on the form, which is exactly what we were expecting. Cucumber
is also telling us what to do next: Add a Twiddr name input field to the
user signup form. To do that, open up app/views/users/_inputs.html.erb
and add a line to include the :twiddr_name input. It should look like
this:
<%= form.inputs do %>
<%= form.input :email %>
<%= form.input :twiddr_name %>
<%= form.input :password %>
<%= form.input :password_confirmation, :label => "Confirm password" %>
<% end %>
Part of the outside-in process is to only write the simplest amount of code that could make the test pass, and not a line more. At this point you may be tempted to go on with implementing the twiddr_name functionality, but it’s important to contain your fingers from typing any further, and instead rerun the feature to see what the next step should be.
Rerunning the feature now shows that not only does our scenario not
pass, but we’ve also broken a different scenario. Don’t panic! All
scenarios that display this form are now broken because the User
model
doesn’t have a twiddr_name
attribute. To add it, we create a database
migration. On your terminal, type:
$ ./script/rails generate migration add_twiddr_name_to_users twiddr_name:string
$ rake db:migrate && rake db:test:prepare
This will add a column called twiddr_name
of type string
to the
users
table. Run the scenario once again, and you’ll note that we’ve
reached a clean build: all tests pass. We should add some more behavior
around our new twiddr_name
. For example, we probably want to require
that new users enter a twiddr_name when signing up to the site. We can
add a scenario that tests for that:
Scenario: User tries to sign up without a twiddr name
When I go to the sign up page
And I fill in "Email" with "email@person.com"
And I fill in "Password" with "password"
And I fill in "Confirm password" with "password"
And I press "Sign up"
Then the "Twiddr name" field should have the "can't be blank" error
Run the scenario, and you will note that the last step is not defined.
Cucumber will even print out a starting point for the step definition.
Copy the provided template and paste it in a new file called
features/step_definitions/form_error_steps.rb
. The definition of this
step will use CSS selectors to verify that the field contains the
expected error message, based on the markup generated by formtastic:
Then /^the "([^"]*)" field should have the "([^"]*)" error$/ do |field_label, error_message|
within('form li') do
page.should have_css("label:contains('#{field_label}') ~ p.inline-errors", :text => error_message)
end
end
Dropping that into the form_error_steps.rb
will allow us to move
forward with our scenario: It fails because we don’t have any
validations on twiddr_name
. We want to always make sure that
twiddr_name
is present. Let’s drop to the model spec level to specify
this new behavior. Create a file called spec/models/user_spec.rb
and
let’s specify this requirement:
require 'spec_helper'
describe User, 'valid' do
it { should validate_presence_of :twiddr_name }
end
The validate_presence_of
method is a
shoulda matcher. There are a ton
of useful matchers. For other active record matcher examples, please
refer to the
documentation.
First, watch it fail by running
bundle exec rspec spec/models/user_spec.rb
. To make it pass, we open
up app/models/user.rb
and add the one required validation line:
validates_presence_of :twiddr_name
. Running the spec again will reveal
that we’re once again green. Now we can return back up to the cucumber
level by rerunning our feature:
bundle exec cucumber features/sign_up.feature
. This time we notice
that the feature we are developing is passing, but we broke a few other
features because of invalid users being created, which brings us to
another aspect of testing: factories
.
During testing it is very often required to generate setup data to create context for the test. One great way to manage that is by using a factory library, where we define a few factory templates, and then we ask the factory to create instances of those objects to either get a hold of them in a variable or store them in the database. It then becomes trivial to get a hold of valid instances of models in your application, which is very useful for setting up a test’s context. In this application, we are using factory_girl.
What’s breaking in the above feature is that the user factory is not
including :twiddr_name
when it generates objects. Therefore it is
creating invalid objects, and ActiveRecord is rightfully raising an
error. The fix in this case is to add the new required attribute to the
user factory, found in spec/factories/clearance.rb
. In that file, the
user factory definition could look like so (we simply use the email to
create a twiddr_name):
Factory.define :user do |user|
user.email { Factory.next :email }
user.password { "password" }
user.password_confirmation { "password" }
user.twiddr_name { |u| u.email.split("@").first }
end
At this point, we once again have a green build. We can move on to
another feature (save the following in
features/user_views_profiles.feature
):
Feature: user profiles
In order to see if I'm interested in following other users
As a twiddr user
I can see other user's profiles
Scenario: View a user's profile
Given the following email confirmed user exists:
| twiddr_name | email |
| hgimenez | hgimenez@example.com |
Given I have signed in with "me@example.com/test"
When I go to the profile page for "hgimenez"
Then I should see "hgimenez" within "div.twiddr_name"
And I should see "hgimenez@example.com" within "div.email"
And I should see "Follow"
Notice the first setup step in the above scenario: “Given the following
email confirmed user exists:” followed by a table. Furthermore, if you
run this scenario you will notice that that step passes even though we
haven’t written a step definition for it. What’s going on here is that
Factory Girl conveniently creates step definitions for all of our
factories, making it very easy to create the data required to provide
some context for your tests. In the above example, “email confirmed
user” is the name of a factory defined in spec/factories/clearance.rb
,
and the cucumber
table
that follows maps attributes to values for the records you’re creating.
It’s simpler and therefore recommended to provide values only for the
attributes that you’re interested in for this scenario. So in this case,
we really don’t care what the user’s password is, but we care about the
twiddr name and email because we’re verifying them a few lines down.
When you run this scenario, you will notice that cucumber did not know
how to map the profile page for "hgimenez"
to an actual URL in your
application. To tell cucumber how to do that, we add an entry in
features/support/path.rb
like so:
when /the profile page for "([^\"]+)"/
user = User.find_by_twiddr_name!($1)
user_path(user)
We are using a regular expression to capture the twiddr name provided in
the step definition, and using the captured string to find that user in
the database. On tests it is a good idea to use the “dangerous” versions
of active record finders (ending with !
), so that if the record is not
found, Active Record will raise a clear error message, making debugging
much easier.
Adding that entry in features/support/paths.rb
gets us further in that
step definition. That step still fails though, but this time for a
different reason:
The action 'show' could not be found for Clearance::UsersController (AbstractController::ActionNotFound)
.
In this case, our users controller does not specify a show action. Since
this is a clearance controller, we can extend it by simply creating a
new controller and inheriting from Clearance::UsersController
. This
will allow us to both create new actions or extend the default ones
provided by clearance. Following cucumber’s failure message, our next
step is to create show action on the users controller:
class UsersController < Clearance::UsersController
def show
end
end
But even with this in place, cucumber continues to complaint about the show action being unavailable. It seems like there could be a routing problem, so let’s again move down to the controller spec level and specify our new route:
require 'spec_helper'
describe UsersController, 'routes' do
it { should route(:get, 'users/1').to(:action => 'show', :id => 1) }
end
The spec above is a shoulda controller
matcher.
We have created a new class for the UsersController, and need the user
routes to map to this new class (without the Clearance namespace).
Additionally, clearance has a few nested resources under users that we
will still need. If we look at the clearance routes file, we can pull
out the relevant pieces and include it in our own app’s
config/routes.rb
, where we can get rid of the clearance namespace:
resources :users, :controller => 'users', :only => [:show, :create] do
resource :password,
:controller => 'clearance/passwords',
:only => [:create, :edit, :update]
resource :confirmation,
:controller => 'clearance/confirmations',
:only => [:new, :create]
end
Running the controller spec again will show that we’ve wired up the routes correctly, and running the entire cucumber suite will show that we have not broken any of the nested routes that clearance originally provided. This is just another advantage of having the kind of strong test coverage that is achieved by BDD, and definitely brings confidence in the quality of the application you’re building: We’ve made a somewhat major refactor, and but we’re confident that things are wired up correctly because our test suite passes.
Moving along, we can rerun the feature file, at which point cucumber complaints that there’s no users/show view. Let’s create it!
<h2><%= @user.twiddr_name -%></h2>
<div id="profile">
<div class="twiddr_name"><%= @user.twiddr_name -%></div>
<div class="email"><%= @user.email -%></div>
<div class="follow">Follow</div>
</div>
Running the feature now reveals that the @user
instance variable is
not set. We can assign this instance variable in the users controller
and try again:
def show
@user = User.find(params[:id])
end
Run rake, and find that all specs and scenarios are passing. We can move
on to following users. In features/user_follows_another_user.feature
:
Feature: Follow twiddr users
So that I can see what they have to say
As a twiddr user
I can follow other twiddr users
Scenario: Follow another twiddr user
Given the following email confirmed user exists:
| twiddr_name | email |
| bob | bob@example.com |
And I have signed in with "hgimenez@example.com/test"
When I go to the profile page for "bob"
And I press "Follow bob"
Then I should be on the home page
And I should see "You are now following bob!"
And I should see "bob" within "div.following"
The first failure message is “no button with value or id or text ‘Follow
bob’ found (Capybara::ElementNotFound)“, because the user profile pages
do not have an actual follow button. At this point, we should have some
idea of how we’ll implement user following. A clean RESTful way to go
about it is to create a user_follows resource, and to POST to it when a
user wants to follow someone else. Let’s add a button that posts to that
resource, in app/views/users/show.html.erb
:
><div id="profile">
<div class="twiddr_name"><%= @user.twiddr_name -%></div>
<div class="email"><%= @user.email -%></div>
<div class="follow"><%= button_to("Follow #{@user.twiddr_name}", user_follows_path(@user)) -%> </div>
</div>
Rerunning the feature now shows
"undefined method `user<em>follows</em>path' for #<#<Class:0x103d108e0>:0x103d0a4e0> (ActionView::Template::Error)"
because there is no user_follows
resource or route yet. This is a good
point to step down into RSpec land while we build this resource. We
create spec/controller/follows_controller_spec.rb
and specify a route:
require 'spec_helper'
describe FollowsController, 'routes' do
it { should route(:post, '/users/1/follows').to(:action => 'create', :user_id => 1) }
end
And we can now enter BDD cycle. Run the spec with
bundle exec rspec spec/controllers/follows_controller_spec.rb
and we
get the error "uninitialized constant FollowsController (NameError)"
because we have not created the follows controller. Create it in
app/controllers/follows_controller.rb
with the following content:
class FollowsController < ApplicationController
end
Rerunning the spec, we now see No route matches "/users/1/follows"
.
Great, let’s add that route nested within the users resource, which now
looks like this:
resources :users, :controller => 'users', :only => [:show, :create] do
resource :password,
:controller => 'clearance/passwords',
:only => [:create, :edit, :update]
resource :confirmation,
:controller => 'clearance/confirmations',
:only => [:new, :create]
resource :follows, :only => [:create] # add this
end
Run the spec again to get feedback from RSpec: The test passes so the
route is working as we expected. Back on the cucumber level we rerun the
feature which now finds the button and route, but errors on
"The action 'create' could not be found for FollowsController (AbstractController::ActionNotFound)"
.
Time to bring the follows controller to life with the create action.
Let’s specify the controller behavior in RSpec. We want only signed in users to be able to hit this action, so let’s spec that out:
describe FollowsController, 'POST to create without authenticating' do
before do
post :create, :user_id => 1
end
it { should redirect_to(sign_in_path) }
end
This test makes sure that we redirect to the sign in path if no user is
authenticated. To make it pass, we simply add
before_filter :authenticate
to our controller. Now on to the more
details about the create action: We want to verify that the signed in
user ends up following the provided user when the action runs.
In spec’s setup we create two users, we simulate authenticating by
assigning controller’s the current_user, and we exercise the controller
by invoking a POST to create. We then verify two things: that the user
is indeed following the provided user, and that we redirect to the root
path. Note that we still don’t have any logic or even models to hold the
user following relationships, but we can start to think about how that
interface will look like. One of these tests already shows some of that:
current_user.follows?
.
describe FollowsController, 'authenticated POST to create' do
let(:follower) { Factory(:email_confirmed_user) }
let(:user_to_follow) { Factory(:email_confirmed_user) }
before do
controller.current_user = follower #sign in
post :create, :user_id => user_to_follow.id
end
it { should redirect_to(root_path) }
it 'makes the signed in user follow the provided user' do
controller.current_user.follows?(user_to_follow).should be_true
end
end
We run this test, watch it fail, and come up with a first pass at an implementation:
def create
user = User.find(params[:user_id])
current_user.follow(user)
redirect_to :root_path
end
This gets us a little further, because the redirect spec now passes.
It’s time to specify our models, and how the follow
method on the user
class will behave. We will also need a follows?
method that determines
whether a user follows another one. Let’s spec this out
spec/models/user_spec.rb
.
describe User, '#follows?' do
subject { Factory(:email_confirmed_user) }
let(:followed_user) { Factory(:email_confirmed_user) }
let(:another_user) { Factory(:email_confirmed_user) }
before do
subject.follow(followed_user)
end
it 'returns true when it follows the user' do
subject.follows?(followed_user).should be_true
end
it 'returns false when it does not follow the user' do
subject.follows?(another_user).should be_false
end
end
describe User, 'following another user' do
subject { Factory(:email_confirmed_user) }
let(:user_to_follow) { Factory(:email_confirmed_user) }
before do
subject.follow(user_to_follow)
end
it 'makes the user follow the provided user' do
subject.follows?(user_to_follow).should be_true
end
it 'does not make the provided user follow the user' do
user_to_follow.follows?(subject).should be_false
end
end
We can model user followings with a simple has_many association on the user model. Let’s specify that on the valid user context using another shoulda matcher:
describe User, 'valid' do
it { should validate_presence_of :twiddr_name }
it { should have_many :followings } # add this
end
Our error message is now: “Expected User to have a has_many association
called followings (no association called followings)“. Let’s add the
followings table with a database migration. Run
./script/rails generate migration create_followings
and add the
following definition:
class CreateFollowings < ActiveRecord::Migration
def self.up
create_table :followings do |t|
t.references :user
t.integer :followed_user_id
t.timestamps
end
add_index :followings, :user_id
end
def self.down
drop_table :followings
end
end
Run rake db:migrate && rake db:test:prepare
to create the table. We
can now create a Following spec and model, where we probably want to
validate presence of user_id:
require 'spec_helper'
describe Following, 'valid' do
it { should validate_presence_of(:user_id) }
it { should validate_presence_of(:followed_user_id) }
it { should belong_to(:user) }
it { should belong_to(:followed_user) }
end
class Following < ActiveRecord::Base
validates_presence_of :user_id
validates_presence_of :followed_user_id
belongs_to :user
belongs_to :followed_user, :class_name => 'User'
end
We finally add the association on our user model to make the test pass:
class User
has_many :followings
end
We are now ready to write up the implementation of the #follows?
and
the #follow
methods on the User class:
def follow(another_user)
self.followings.create(:followed_user_id => another_user.id)
end
def follows?(another_user)
self.followings.exists?(['followed_user_id = ?', another_user])
end
Now we can move up to the following controller spec, and guess what: It passes. When we wrote the controller spec and the controller code we thought about what the API for following should look like, then we specified it at the model level and wrote an implementation which adhered to the contract we had established earlier.
We can move up one more level to the cucumber feature. Running it, we
see the following error:
"expected #has_content?("You are now following bob!") to return true, got false (RSpec::Expectations::ExpectationNotMetError)"
.
We can make that pass by adding a flash message on our controller
action:
flash[:notice] = "You are now following #{user.twiddr_name}!"
We now have one more failing step, which is verifying that the home page
displays the users we are following. So far we haven’t really created a
home or welcome page, and have been staying with the default root path
provided by clearance (Clearance::Sessions#new
). Let’s specify that
controller’s routes in spec/controllers/welcome_controller.rb
:
describe WelcomeController, 'routes' do
it { should route(:get, '/').to(:action => 'index') }
end
To make that pass, we create the WelcomeController
class in
app/controllers/welcome_controller.rb
, and add the following routes to
config/routes.rb
:
>root :to => 'welcome#index'
resource :welcome, :only => [:index]
Now let’s specify some behavior for our welcome controller. When the user is not logged in, we want to redirect to the sign in page, otherwise we just render the welcome page:
describe WelcomeController, 'GET to index without authenticating' do
before do
get :index
end
it { should redirect_to(sign_in_path) }
end
describe WelcomeController, 'GET to index when logged in' do
let(:user) { Factory(:email_confirmed_user) }
before do
controller.current_user = user
get :index
end
it { should render_template(:index) }
end
Making this pass is quite simple. We’ve already seen how to require
authentication with clearance: before_filter :authenticate
. We also
add an empty index action, and create the
app/views/welcome/index.html.erb
file, currently empty, where we can
start creating twiddr’s welcome page.
Now we rerun our cucumber feature which once again reminds us that we need to list out the user’s followings in the welcome page. Easy peasy:
><h3>You follow</h3>
<ul>
<% current_user.followings.each do |following| %>
<li class="following"><%= following.followed_user.twiddr_name -%></li>
<% end %>
</ul>
At this point, we also realize that an <li>
tag is more appropriate
than a <div>
for a list of followings, so we also change the scenario
to look for an li
and not a div
. Therefore that verification steps
looks like so:
And I should see "bob" within "li.following"
Run the feature. It’s all green. Run all specs and cucumber scenarios.
We see one failure:
"expected #has_content?("Follow") to return true, got false (RSpec::Expectations::ExpectationNotMetError)"
.
Not a big deal: we changed the Follow text to an actual button, so we
need to change our feature accordingly. The step can now look like this:
And I should see the "Follow" button
The step definition is quite simple, because Capybara has an easy way to find and verify buttons on a page:
Then /^I should see the "([^"]*)" button$/ do |button_label|
page.should have_button(button_label)
end
We can now rerun the feature, as well as the entire suite with just
rake
, and behold a clean build once again.
What we’ve learned
We have started building our own social network using the Outside-in development methodology with Cucumber, Rspec and other great tools. If you’ve followed along and actually built this app, you’ve experienced first hand what the process feels like, and hopefully you’ve started to see the benefit of writing tests first and value of Outside-in development:
- We’ve already encountered places where a small refactor was required, and our tests gave us the confidence to know that everything is still working as intended. More significant refactors or code optimizations down the road will also be much easier because we have significant test coverage in the app.
- We build more coherent and simpler UIs and workflows, because it approaches code from the stakeholder’s and business value perspectives.
- While the ability to refactor with confidence is a huge win, we’ve also seen how the process also helps designing the system. We’ve used cucumber for acceptance tests, and used RSpec’s tighter feedback loop for more fine grained internal design of the APIs. BDD allows us to focus on the task at hand, as the primary goal is to make the test pass.
- We constantly get feedback from the system while developing new features, so we always know if what we’ve written is correct, and where to go next.
- We write small pieces of code that make a test pass. Bad code is hard to test, so if we write the test first, we are less likely to write complex code. Therefore our app will be easier to maintain on the long run.
- If we follow and embrace BDD, we are keeping the test coverage in our app high, which will help catch regression bugs when new functionality is added and the application gets more complex.
I hope that this introduction to Outside-in development inspires you to continue to learn more about the method and the numerous tools that support it. I encourage you to continue to build out this app – or any other project – so that you can continue to get better until you are able to experience the development flow that can be achieved with BDD. Feel free to ask questions and give feedback in the comments section of this post. Thanks!
Do read these awesome Guest Posts:
- Ruby Forensics
- An introduction to eventmachine, and how to avoid callback spaghetti
- The Testing Mindset
- An Introduction to Desktop Apps with Ruby
- The Ruby movement
- Almost everything is an object (and everything is almost an object!)
- So… you’re new to Ruby!
- Incorporating Web APIs to spark computer programming exercises
- 14 Ways To Have Fun Coding Ruby
- Writing modular web applications with Rack
- How to Learn Ruby (or any programming language)