Today I gave a presentation at Desert Code Camp about BDD, Cucumber, Webrat and User Stories. If you’d like to find review the slides or download the code I used during the presentation here they are.
Cucumber Table Transformations with Factory Girl Sequences
If you’re using Cucumber and you’re not using Transformations you’re doing it wrong. I just started using these recently and ran into a problem with creating records using factory_girl factories that made use of sequences. While trying to create multiple Authlogic user records with a unique email and unique single_access_token using a Cucumber table, the functionality of Factory.next(:email) wasn’t working correctly, I would always get the same e-mail address. Turns out it was an easy fix, just had to use lazy attributes in my factory.
The Scenario, Step Definition and Factory
1 2 3 4 5 6 7 8 9 10 11 12 | Scenario: Presenter List
Given the following presenters:
| Name | Bio | Website |
| Clayton | Rails dev @integrum | http://claytonlz.com |
| Chris | Scrum @integrum | |
And I am on the homepage
When I follow "Presenters"
Then I should see "Clayton"
And I should see "Rails dev @integrum"
And I should see "http://claytonlz.com"
Then I should see "Chris"
And I should see "Scrum @integrum" |
My Step Definition
This uses the Transformation table below:
1 2 3 4 5 | Given /^the following presenters:$/ do |table| table.each do |attrs| Factory.create(:user, attrs) end end |
This transformation takes a table like the one in my scenario above, and assigns the values to a hash using the actual model attribute names (Name isn’t an attribute on a user but name is). The regular cucumber step definition “consumes” this hash for each entry in the table and passes it to a Factory for creation.
1 2 3 4 5 | Transform /^table:Name,Bio,Website$/ do |table| table.hashes.map do |hash| {:name => hash[:Name], :bio => hash[:Bio], :website => hash[:Website]} end end |
My Factory
This is a pretty basic factory for an authlogic user model, I’m using factory_girl sequences to give me a “unique” e-mail and single access token, which are required by Authlogic.
1 2 3 4 5 6 7 8 9 | Factory.define :user do |user| user.email Factory.next(:email) user.name "" user.bio "" user.website "" user.password "password" user.password_confirmation "password" user.single_access_token Factory.next(:single_access_token) end |
The problem
The above scenario will fail when it tries to create the user records via the factories. You’ll see a validation error about how the user model requires a unique e-mail and single access token. You’ll be wondering, “hey why are my sequences working?”. When you inspect the log you’ll see that they are in fact NOT working.
The Quick Answer
The easy answer to this is that you need to use lazy attributes in your factory for the sequences so that they are loaded each time instead of once.
1 2 3 4 5 6 7 8 9 | Factory.define :user do |user| user.email { Factory.next(:email) } user.name "" user.bio "" user.website "" user.password "password" user.password_confirmation "password" user.single_access_token { Factory.next(:single_access_token) } end |
Notice the curly braces around the sequences
The Longer Answer
The cucumber rdoc explains the Transform functionality, albeit somewhat hard to understand.
Registers a proc that will be called with a step definition argument if it matches the pattern passed as the first argument to Transform. Alternatively, if the pattern contains captures then they will be yielded as arguments to the provided proc. The return value of the proc is consequently yielded to the step definition.
I think the issue comes from something with the way these Procs are created, called and also their scope with regard to the step definition etc. I don’t think my ruby-fu is strong enough to give a good explanation but maybe I’m going in the right direction.
Missing host to link to! Please provide :host parameter
If you’ve followed my version of the authlogic account activation tutorial or the original version by Matt Hooks you might have run into this error:
Missing host to link to! Please provide :host parameter or set default_url_options[:host] when sending emails
When authlogic sends e-mails with the account activation link, it uses a url_for helper to build that link. Because the “Notifier” mailer is an instance of ActionMailer::Base and not ActionController::Base it doesn’t know what the host parameter of the URL should be, so you have to tell it explicitly.
Put the following into your environments/development.rb and environments/test.rb:
1 2 | # This assumes you're running your local development server on port 3000 via script/server config.action_mailer.default_url_options = { :host => "127.0.0.1:3000" } |
Put this into your environments/production.rb:
1 2 | # Replace example.org with your actual domain name config.action_mailer.default_url_options = { :host => "example.org" } |
Make Fun of Your Client To Prevent Defects
One thing I’ve always heard about learning a new language is that you can’t consider yourself fully fluent until you can tell a joke. Telling a joke requires the understanding of homonyms and how words flow together. “So a guy walks into a bar…” sounds different than “A man enters beverage store…”.
When dealing with clients, it’s easy to joke about your client, but don’t miss an opportunity to prevent future missteps once you know more about them.
How often do you get off the phone with a client and immediately have a funny remark about some critique or issue they have mentioned. Typically this manifests itself when the client brings up an issue, that while important to them, is considered trivial by the developer.
Can you believe this guy!? I built out this whiz-bang feature and he’s just complaining about the font being too small!
The key part of this interaction is that you’ve now got a little insight into what makes this particular client tick. When you demo the next few features and he is unimpressed by the functionality, but comments on the spacing of form elements you should skip the joke and make a mental note for the future.
Once you can tell a joke about your client, before the interaction, and you have an “I told ya so!” moment with yourself or your pair afterwards, you know you’ve made it to the next level of understanding that client. Now, the next time you deploy a feature, discuss requirements or ask a question you can preempt the inevitable by accounting for those quirks and personal preferences of your client.
Instead of making a joke, make them happy, then you can both smile.
Authlogic Account Activation Tutorial
I found a great tutorial explaining how to setup user activation with authlogic, but it was a little hard to read so I’ve dumped it all into one easier to read file.
Here’s the original: http://github.com/matthooks/authlogic-activation-tutorial/tree/master
Here’s my fork on github: http://github.com/clayton/authlogic-activation-tutorial/tree/master
Below is the formatted guide that is on github.
Remember, all credit goes to Matt Hooks and his original Authlogic Account Activation Tutorial
Introduction
This is an easier to read version of Matt Hooks’ Authlogic Activation Tutorial. The tutorial is divided into a number of steps and walks through the process of implementing user activation functionality into your pre-existing Rails app using Authlogic. If you are just starting out with Authlogic, be sure to checkout the Authlogic Example Tutorial.
Step 1
Let’s begin by adding an ‘active’ field with a default of false to the user model.
script/generate migration AddActiveToUsers active:boolean
1 2 3 4 5 6 7 8 9 10 | # new migration XXX_add_active_to_users.rb class AddActiveToUsers < ActiveRecord::Migration def self.up add_column :users, :active, :boolean, :default => false, :null => false end def self.down remove_column :users, :active end end |
Step 2
Don’t forget to run the migraiton.
rake db:migrate
Authlogic automatically executes the following methods, if present, upon user action: active?, approved?, and confirmed?. Let’s create an “active?” method so we can hook into this magical goodness. And we should make sure that we protect the active attribute from mass-assignments by calling attr_accessible.
1 2 3 4 5 6 | # added to user.rb attr_accessible :login, :email, :password, :password_confirmation, :openid_identifier def active? active end |
Step 3
Now try to log in. You should receive the error, “Your account is not active.” So far so good. Let’s make a controller to handle our activations:
script/generate controller activations new create
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # new file app/controllers/activations_controller.rb class ActivationsController < ApplicationController before_filter :require_no_user, :only => [:new, :create] def new @user = User.find_using_perishable_token(params[:activation_code], 1.week) || (raise Exception) raise Exception if @user.active? end def create @user = User.find(params[:id]) raise Exception if @user.active? if @user.activate! @user.deliver_activation_confirmation! redirect_to account_url else render :action => :new end end end |
Step 4
I raise exceptions in these actions to make sure that someone who is already active cannot re-activate their account and to deal with an invalid perishable token. I’ll leave it up to you how you want to handle these errors — you should probably provide some sort of “My Token is Expired!” action that will reset the token and resend the activation email if the user does not get around to activating right away.
Going down the list, let’s define the missing actions. First:
1 2 3 4 5 | # added to user.rb def activate! self.active = true save end |
Step 5
Next, let’s make sure our user gets an e-mail with his activation code when he signs up. How are we getting our activation code? The same way we get our password reset code — through our perishable token:
Update: If you are experiencing the “Missing host to link to! Please provide :host parameter ” error see this post Missing host to link to! Please provide :host parameter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | # added to app/models/user.rb
def deliver_activation_instructions!
reset_perishable_token!
Notifier.deliver_activation_instructions(self)
end
def deliver_activation_confirmation!
reset_perishable_token!
Notifier.deliver_activation_confirmation(self)
end
# added to app/models/notifier.rb
def activation_instructions(user)
subject "Activation Instructions"
from "Binary Logic Notifier <noreply@binarylogic.com>"
recipients user.email
sent_on Time.now
body :account_activation_url => register_url(user.perishable_token)
end
def activation_confirmation(user)
subject "Activation Complete"
from "Binary Logic Notifier <noreply@binarylogic.com>"
recipients user.email
sent_on Time.now
body :root_url => root_url
end
# added to config/routes.rb
map.register '/register/:activation_code', :controller => 'activations', :action => 'new'
map.activate '/activate/:id', :controller => 'activations', :action => 'create'
<!-- new file app/views/notifier/activation_instructions.erb -->
Thank you for creating an account! Click the url below to activate your account!
<%= @account_activation_url %>
If the above URL does not work try copying and pasting it into your browser. If you continue to have problem, please feel free to contact us.
<!-- new file app/views/notifier/activation_confirmation.erb -->
Your account has been activated.
<%= @root_url %>
If the above URL does not work try copying and pasting it into your browser. If you continue to have problem, please feel free to contact us. |
Step 6
Now let’s modify the user create action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # modified app/controllers/users_controller.rb def create @user = User.new(params[:user]) # Saving without session maintenance to skip # auto-login which can't happen here because # the User has not yet been activated if @user.save_without_session_maintenance @user.deliver_activation_instructions! flash[:notice] = "Your account has been created. Please check your e-mail for your account activation instructions!" redirect_to root_url else render :action => :new end end |
Step 7
As the comment says, we don’t need the Authlogic auto-login to take place so we save without maintaining the session. Now let’s define the ‘register’ view.
1 2 3 4 5 6 7 8 | <!-- new file app/views/activations/new.html.erb --> <h1>Activate your account</h1> <% form_for @user, :url => activate_path(@user.id), :html => { :method => :post } do |f| %> <%= f.error_messages %> <%= f.submit "Activate" %> <% end %> |
Step 8
Let’s see if things are working…
… (processing) …
Looks like our user got activated!
But there’s a slight problem. Since we didn’t update the user’s password, we didn’t get a magical Authlogic auto-login! How rude.
At this point it’s perfectly fine to let the user log themselves in. And you can certainly simplify the activation down to one action so the user doesn’t have to click another button. But, I like Authlogic’s session maintenance. I also like short signup forms. So let’s kill two birds with one stone.
Let’s set up the user creation form to only ask for a user’s login/email. Then, let’s ask the user to set their password/openid upon activation, which will log them in automatically.
First, let’s change our acts_as_authentic call to only check for password length on update if the user has no credentials set.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | # modified user.rb # For authlogic 2.0+ acts_as_authentic do |c| c.validates_length_of_password_field_options = {:on => :update, :minimum => 4, :if => :has_no_credentials?} c.validates_length_of_password_confirmation_field_options = {:on => :update, :minimum => 4, :if => :has_no_credentials?} end # Pre-authlogic 2.0 # acts_as_authentic :login_field_validation_options => { :if => :openid_identifier_blank? }, # :password_field_validation_options => { :if => :openid_identifier_blank? }, # :password_field_validates_length_of_options => { :on => :update, :if => :has_no_credentials? } # ... # we need to make sure that either a password or openid gets set # when the user activates his account def has_no_credentials? self.crypted_password.blank? && self.openid_identifier.blank? end # ... # now let's define a couple of methods in the user model. The first # will take care of setting any data that you want to happen at signup # (aka before activation) def signup!(params) self.login = params[:user][:login] self.email = params[:user][:email] save_without_session_maintenance end # the second will take care of setting any data that you want to happen # at activation. at the very least this will be setting active to true # and setting a pass, openid, or both. def activate!(params) self.active = true self.password = params[:user][:password] self.password_confirmation = params[:user][:password_confirmation] self.openid_identifier = params[:user][:openid_identifier] save end # modified activations_controller.rb def create @user = User.find(params[:id]) raise Exception if @user.active? if @user.activate!(params) @user.deliver_activation_confirmation! flash[:notice] = "Your account has been activated." redirect_to account_url else render :action => :new end end # modified users_controller.rb def create @user = User.new if @user.signup!(params) @user.deliver_activation_instructions! flash[:notice] = "Your account has been created. Please check your e-mail for your account activation instructions!" redirect_to root_url else render :action => :new end end |
Step 10
Now we need to update our views to reflect the new signup process.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!-- modified app/views/activations/new.html.erb --> <h1>Activate your account</h1> <% form_for @user, :url => activate_path(@user.id), :html => { :method => :post } do |form| %> <%= form.error_messages %> <%= render :partial => "form", :locals => { :form => form }%> <%= form.submit "Activate" %> <% end %> <!-- new file app/views/activations/_form.html.erb --> <%= form.label :password, "Set your password" %><br /> <%= form.password_field :password %><br /> <br /> <%= form.label :password_confirmation %><br /> <%= form.password_field :password_confirmation %><br /> <br /> <%= form.label :openid_identifier, "Or use OpenID instead of your email / password" %><br /> <%= form.text_field :openid_identifier %><br /> |
The End
And that’s it! Let me know if you have any suggestions for improvement.
How To: Setup RSpec, Cucumber, Webrat, RCov and Autotest on Leopard
RSpec, Cucumber, Webrat, RCov and Autotest are a powerful combination of tools for testing your Rails app. Unfortunately getting them to all work nicely together can be a bit of challenge. I recently configured a development environment from scratch on OS X 10.5 Leopard and kept track of all of the little details.
Prerequisites
I’m assuming you’ve got the following installed:
- ruby
- ruby gems 1.3.1
- Apple development tools
- git
- rails >= 2.3.2
- You’ve added github to your gem sources (gem sources -a http://gems.github.com)
RSpec & RSpec-Rails
First let’s grab the rspec1 and rspec-rails2 gems.
sudo gem install rspec
sudo gem install rspec-rails
Cucumber
Next we’ll install the cucumber3 gem
sudo gem install cucumber
Webrat
Webrat4 is used by cucumber to simulate a browser for your integration tests. Webrat will also install nokogiri5.
sudo gem install webrat
RCov
I thought RCov6 would get installed with RSpec, but it wasn’t for me. You might not need to do this, but just to make sure…
sudo gem install rcov
Autotest
Autotest7 comes from ZenTest8 and allows you to have a kick ass workflow where you are constantly running relevant tests and less-constantly automatically running your entire test suite.
sudo gem install ZenTest
Optionally, Thoughtbot’s Factory Girl
Factory girl9 is a really helpful fixture replacement (and more) gem to use in conjunction with cucumber, checkout their much better explanation
sudo gem install thoughtbot-factory_girl --source http://gems.github.com
Optionally, Carlos Brando’s Autotest Notification
While autotest normally runs in a terminal window, it can be setup to hook into applications like growl or snarl. The Autotest Notification9 gem helps make this setup a lot easier.
You will need growl installed and configured for this step the installation instructions on this gems github page are very easy to follow.
sudo gem install carlosbrando-autotest-notification --source=http://gems.github.com
Next you need to turn autotest notifications “on”
an-install
A Sample Rails App
Let’s create a sample rails app for the rest of this guide.
rails sample-app
Configuring Environment Variables
Autotest relies on some environment variables to run all of your features and specs correctly. If autotest “hangs” after you try to run it, or it just never seems to be watching your specs or features, this will most likely solve your problem.
Open the test.rb environment definition file in sample-app/config/environments/test.rb and add the following.
1 2 | ENV['AUTOFEATURE'] = "true" ENV['RSPEC'] = "true" |
These lines will test autotest to run, and look for changes to, your specs (rather than test unit tests) and your cucumber features.
Update
If you don’t want to add these environment variables to every rails project you’ve got on your machine, you can also choose to set them as environment variables in your .bash_profile or .bashrc (or whatever shell you’re using) files.
export AUTOFEATURE=true export RSPEC=true
Unpacking Gems
Next let’s freeze (unpack) some gems that we’ll be using in our app. I’ve run into problems trying to use the system gems with cucumber, rspec and webrat, especially when I have multiple versions of any of them installed. Unpacking them into my rails app solves this problem for me.
mkdir sample-app/vendor/gems cd sample-app/vendor/gems gem unpack rails gem unpack rspec gem unpack rspec-rails gem unpack cucumber
Because webrat (and nokogiri) are native gems, that is, they are built locally on your machine based on its architecture, we won’t unpack those.
config.gem support
The current accepted practice, when using rails 2.3, and as suggested by the rspec guy(s) is to use rails’ config.gem functionality.
Open sample-app/config/environments/test.rb and add the following lines:
config.gem "rspec", :lib => false, :version => ">= 1.2.0" config.gem "rspec-rails", :lib => false, :version => ">= 1.2.0" config.gem "cucumber", :lib => false, :version => ">= 0.2.3" config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :source => "http://gems.github.com" config.gem "webrat", :lib => false, :version => ">= 0.4.3" config.gem "nokogiri", :lib => false, :version => ">= 1.2.3"
Your version numbers may be different, but these are all current at the time of writing.
Boot Strapping RSpec and Cucumber
Before you can get very far with rspec or cucumber you need to run the bootstrapping scripts to give yourself the default files and directories.
# From inside your rails app sample-app/ script/generate rspec script/generate cucumber
Factories
Depending on where you’re going to use your factories the most, you might want to save your file in either spec/ or features/. I chose the latter. Only complete this step if you plan to use the FactoryGirl gem.
touch sample-app/features/factories.rb
Getting Accurate RCov Data
By default RCov is setup to only use your specs when calculating code coverage. If you’re using Cucumber and RSpec, you’ll obviously want to include both types of tests to calculate your project’s true code coverage.
I picked up this rcov rake task from my co-worker Jay McGavren it does all of the heavy lifting for you, we’ll just need to make a couple of changes.
Drop this file into sample-app/lib/tasks/rcov.rake and use it by calling rake rcov:all from your terminal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | require 'cucumber/rake/task' #I have to add this require 'spec/rake/spectask' namespace :rcov do Cucumber::Rake::Task.new(:cucumber) do |t| t.rcov = true t.rcov_opts = %w{--rails --exclude osx/objc,gems/,spec/,features/ --aggregate coverage.data} t.rcov_opts << %[-o "coverage"] end Spec::Rake::SpecTask.new(:rspec) do |t| t.spec_opts = ['--options', ""#{RAILS_ROOT}/spec/spec.opts""] t.spec_files = FileList['spec/**/*_spec.rb'] t.rcov = true t.rcov_opts = lambda do IO.readlines("#{RAILS_ROOT}/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten end end desc "Run both specs and features to generate aggregated coverage" task :all do |t| rm "coverage.data" if File.exist?("coverage.data") Rake::Task["rcov:cucumber"].invoke Rake::Task["rcov:rspec"].invoke end end |
The important part here is on line 7, we want rcov to exclude our features directory. We obviously don’t need or want rcov telling us that our feature files are not “covered”. To solve this problem we’ve simply excluded the features directory from rcov’s processing.
We also need to slightly modify sample-app/spec/rcov.opts to get the full rspec + cucumber coverage data.
Your rcov.opts should look like this:
--exclude "spec/*,gems/*,features/*" --rails --aggregate "coverage.data"
We again want to ignore our cucumber features and we also want to tell rcov to aggregate data in a file called coverage.data. This is used in the above rake task.
Write Some Specs and Features!
Act like you know what you’re doing and write some models, controllers whatever. Add some specs and features too.
Autotest Workflow
Open a terminal and make your way to your sample rails app and fire up autotest. You might see something like the following, depending on how many specs and features you’ve got.
$> autotest loading autotest/cucumber_rails_rspec opts ... Finished in 0.06276 seconds 3 examples, 0 failures ================================================================================ /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby /Library/Ruby/Gems/1.8/gems/cucumber-0.2.3/bin/cucumber --format progress --format rerun --out /var/folders/Aq/Aqp06i3dFnqse+tQgQA+1++++TI/-Tmp-/autotest-cucumber.75956.0 features ................. 4 scenarios 17 passed steps /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby /Library/Ruby/Gems/1.8/gems/rspec-1.2.2/bin/spec --autospec spec/models/intern_spec.rb -O spec/spec.opts ... Finished in 0.062995 seconds 3 examples, 0 failures ================================================================================ /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby /Library/Ruby/Gems/1.8/gems/cucumber-0.2.3/bin/cucumber --format progress --format rerun --out /var/folders/Aq/Aqp06i3dFnqse+tQgQA+1++++TI/-Tmp-/autotest-cucumber.75956.1 features ................. 4 scenarios 17 passed steps
The REALLY important stuff
- make sure you’ve got “ENV['AUTOFEATURE'] = true” in your test.rb otherwise autotest won’t run your features automatically
- make sure you’ve got “ENV['RSPEC'] = true” in your bash profile or else autotest won’t run your specs automatically
- make sure you’ve got “–aggregate = ‘coverage.data’” in your spec/rcov.opts file if you’re going to use the above rake task and hope to get combined rcov coverage data between rspec and cucumber
- make sure you’re excluding the features directory from rcov where required or else you’ll end up with misleading rcov data.
Gem Versions
Here’s a list of the current gems and their versions that I used in preparing this guide.
*** LOCAL GEMS *** actionmailer (2.3.2, 1.3.6, 1.3.3) actionpack (2.3.2, 1.13.6, 1.13.3) actionwebservice (1.2.6, 1.2.3) activerecord (2.3.2, 1.15.6, 1.15.3) activeresource (2.3.2) activesupport (2.3.2, 1.4.4, 1.4.2) acts_as_ferret (0.4.1) addressable (2.0.2) builder (2.1.2) capistrano (2.0.0) carlosbrando-autotest-notification (1.9.1) cgi_multipart_eof_fix (2.5.0, 2.2) cucumber (0.2.3) daemons (1.0.9, 1.0.7) data_objects (0.9.11) diff-lcs (1.1.2) dnssd (0.6.0) extlib (0.9.11) fastthread (1.0.1, 1.0) fcgi (0.8.7) ferret (0.11.4) gem_plugin (0.2.3, 0.2.2) highline (1.2.9) hpricot (0.6) libxml-ruby (0.3.8.4) mongrel (1.1.4, 1.0.1) mysql (2.7) needle (1.3.0) net-sftp (1.1.0) net-ssh (1.1.2) nokogiri (1.2.3) polyglot (0.2.5) rack (0.9.1) rails (2.3.2, 1.2.6, 1.2.3) rake (0.8.4, 0.7.3) rcov (0.8.1.2.0) RedCloth (3.0.4) rspec (1.2.2) rspec-rails (1.2.2) ruby-openid (1.1.4) ruby-yadis (0.3.4) rubynode (0.1.3) sources (0.0.1) sqlite3-ruby (1.2.1) term-ansicolor (1.0.3) termios (0.9.4) textmate (0.9.2) thor (0.9.9) thoughtbot-factory_girl (1.2.0) treetop (1.2.5) webrat (0.4.3) ZenTest (4.0.0)
El Fin
Hopefully this guide was useful or had that one little step that you needed to get everything working. I’m sure this will all be out of date in the coming weeks, but I’ll try to keep it as up-to-date as possible. If you see any errors, or can better explain some of the missing pieces, please post a comment. Thanks!
1 http://github.com/dchelimsky/rspec/tree/master
2 http://github.com/dchelimsky/rspec-rails/tree/master
3 http://github.com/aslakhellesoy/cucumber/tree/master
4 http://wiki.github.com/brynary/webrat
5 http://github.com/tenderlove/nokogiri/tree/master
6 http://rubyforge.org/projects/rcov/
7 http://www.zenspider.com/ZSS/Products/ZenTest/#rsn
8 http://www.zenspider.com/ZSS/Products/ZenTest/
9 http://github.com/thoughtbot/factory_girl/tree/master
10 http://github.com/carlosbrando/autotest-notification/tree/master
Updates
2009-12-08 – Removed “sudo” when describing how to unpack gems (h/t xdotcommer)
FIX: GoDaddy VPS 554 too many hops Error
Here’s a quick fix for the “Remote host said: 554 too many hops, this message is looping (#5.4.6)” error message you might get when trying to send an e-mail to an e-mail address configured in PLESK on a GoDaddy Linux VPS running Qmail.
- SSH Into your GoDaddy VPS
- Copy the file located at
/var/qmail/control/smtproutesto/var/qmail/control/smtproutes.old - Open the file located at
/var/qmail/control/smtproutes - Delete its contents
I believe this will only work if you’re not using GoDaddy to host any of your e-mail and you only want your VPS and PLESK (running Qmail) to be responsible for hosting your e-mail.
More information and the original fix can be found here: http://www.datapencil.com/horde.htm
Ruby Gotcha Using case to Compare Classes
I was working on some code today that makes use of the activerecord-activesalesforce gem to connect to SalesForce and pull records for processing in my rails app. I needed to handle some types of records differently than others, so I decided to use a case statement to process the different SalesForce records accordingly.
I ran into a problem comparing classes, my code looked something like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | [Array, String, Integer].each do |klass| case klass when Array puts "Array here!" else puts "No Array here." end end # Resulted In # No Array Here. # No Array Here. # No Array Here. |
Not exactly what I expected. Luckily, my genius rock-star-programmer pair realized that Ruby’s case must be using = rather than .
Changing the code to the following solved my problem.
1 2 3 4 5 6 7 8 9 10 11 12 13 | [Array, String, Integer].each do |klass| case klass when klass == Array puts "Array here!" else puts "No Array here." end end # Results # Array here! # No Array here. # No Array here. |
Next up is figuring out why === behaves like this when comparing classes.
1 2 3 4 | Array === Array #false Array == Array #true #why? |
Why I switched to nginx
When I originally setup my VPS I didn’t think twice about using Apache for my web serving duties. Setting up the LAMP environment was very easy and everything that I needed to host worked perfectly with this setup. However, when I decided to create my blog using Rails, I knew I would have to modify my setup. Originally I had planned to use mod_proxy_balancer and mongrel, but then Phusion’s Passenger came out and that was easy enough to get running. However, I was itching to try something new, improve the speed of my sites and lower the amount of resources that were being used on my Slice. Enter Nginx.
Here’s why I decided to give Nginx a shot:
- Nginx is lightweight and has a small memory footprint
- Nginx is super fast
- Nginx can be easily configured to send rails requests to mongrel, thin and others
- Nginx has easy to read configuration logs and rewriting rules
- I get to learn something new!
Note: All ApacheBench test results displayed are the from the 4th consecutive test. This minimizes the negative effects of the slow startup of ruby processes with Phusion Passenger where the initial requests take a considerably long time.
Apache vs. Nginx Memory Usage
For this test I ran ApacheBench against the index page of a well trafficked PunBB forum that I host on my slice. I fired up top and found the following:
Apache
Mem: 262316k total, 207140k used, 55176k free, 4280k buffers
Nginx
Mem: 262316k total, 146956k used, 115360k free, 3844k buffers
That’s about 40MB less with Nginx. That makes a big difference when you’re only starting out with 256MB.
Apache vs. Nginx AB results
Apache
This is ApacheBench, Version 2.0.41-dev <$Revision: 1.141 $> apache-2.0 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ Benchmarking www.example.com (be patient).....done Server Software: Apache Server Hostname: www.example.com Server Port: 80 Document Path: / Document Length: 8857 bytes Concurrency Level: 10 Time taken for tests: 1.732965 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 928532 bytes HTML transferred: 897838 bytes Requests per second: 57.70 [#/sec] (mean) Time per request: 173.296 [ms] (mean) Time per request: 17.330 [ms] (mean, across all concurrent requests) Transfer rate: 522.80 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 22 26 4.3 26 40 Processing: 70 136 41.5 133 268 Waiting: 46 105 40.9 100 239 Total: 93 163 42.4 160 294 Percentage of the requests served within a certain time (ms) 50% 160 66% 174 75% 186 80% 197 90% 213 95% 250 98% 274 99% 294 100% 294 (longest request)
Nginx
This is ApacheBench, Version 2.0.41-dev <$Revision: 1.141 $> apache-2.0 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ Benchmarking www.example.com (be patient).....done Server Software: Nginx/0.6.32 Server Hostname: www.example.com Server Port: 8080 Document Path: / Document Length: 8858 bytes Concurrency Level: 10 Time taken for tests: 1.107068 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 929232 bytes HTML transferred: 897920 bytes Requests per second: 90.33 [#/sec] (mean) Time per request: 110.707 [ms] (mean) Time per request: 11.071 [ms] (mean, across all concurrent requests) Transfer rate: 819.28 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 22 25 4.3 25 42 Processing: 62 80 29.4 75 345 Waiting: 37 49 10.6 47 93 Total: 85 106 29.5 100 368 Percentage of the requests served within a certain time (ms) 50% 100 66% 105 75% 109 80% 112 90% 119 95% 146 98% 147 99% 368 100% 368 (longest request)
There are a few things that stand out to me from these tests:
- Requests Per Second: 90 vs 58, Nginx wins
- Mean Processing Time: 80 vs 136, Nginx wins
- Percentage of Requess served within 100ms: 50% vs. ??, Nginx Wins
Nginx and Thin vs. Apache and Phusion Passenger
So the PHP/MySQL stuff was all fine and good and that’s something that Apache and Nginx do very well, so no real surprises there. What about handling requests to a simple rails application like this blog?
For this test I again ran apache bench against the non-cached version of my root url. On this page there are some partials being rendered, active record database calls, images loading from Amazon’s S3 and some static files being served.
Apache 2.2 + Phusion Passenger 2.0.3
This is ApacheBench, Version 2.0.41-dev <$Revision: 1.141 $> apache-2.0 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ Benchmarking www.lengelzigich.com (be patient).....done Server Software: Apache Server Hostname: www.lengelzigich.com Server Port: 80 Document Path: / Document Length: 14894 bytes Concurrency Level: 10 Time taken for tests: 26.118296 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 1544644 bytes HTML transferred: 1493235 bytes Requests per second: 3.83 [#/sec] (mean) Time per request: 2611.830 [ms] (mean) Time per request: 261.183 [ms] (mean, across all concurrent requests) Transfer rate: 57.74 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 22 24 1.5 25 27 Processing: 769 2477 389.8 2524 3076 Waiting: 744 2448 387.8 2498 3048 Total: 792 2501 390.1 2547 3102 Percentage of the requests served within a certain time (ms) 50% 2547 66% 2637 75% 2793 80% 2819 90% 2906 95% 2963 98% 3079 99% 3102 100% 3102 (longest request)
Nginx + Thin
I decided to use thin instead of mongrel because, well, I guess I just wanted to try something new. I heard that thin had recently hit version 1.0 and figured it was worth checking out.
This is ApacheBench, Version 2.0.41-dev <$Revision: 1.141 $> apache-2.0 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ Benchmarking www.lengelzigich.com (be patient).....done Server Software: Nginx/0.6.32 Server Hostname: www.lengelzigich.com Server Port: 80 Document Path: / Document Length: 14894 bytes Concurrency Level: 10 Time taken for tests: 17.334487 seconds Complete requests: 100 Failed requests: 0 Write errors: 0 Total transferred: 1534900 bytes HTML transferred: 1489400 bytes Requests per second: 5.77 [#/sec] (mean) Time per request: 1733.449 [ms] (mean) Time per request: 173.345 [ms] (mean, across all concurrent requests) Transfer rate: 86.42 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 22 24 1.4 25 26 Processing: 526 1663 489.6 1544 2995 Waiting: 477 1613 491.6 1496 2949 Total: 549 1687 489.9 1567 3020 Percentage of the requests served within a certain time (ms) 50% 1567 66% 1882 75% 1947 80% 2034 90% 2394 95% 2648 98% 3020 99% 3020 100% 3020 (longest request)
The results here are a little more impressive.
- Requests Per Second: 6 vs. 4, not great, but Nginx wins
- Mean Processing time: 1660 vs. 2448, Nginx wins
- Percentage of Requess served within 1560ms: 50% vs. ??, Nginx Wins
I don’t have the top memory output for these two tests handy, but with Apache free memory was hovering around 3MB. Nginx had free memory hovering around 12MB.
The key difference between Nginx and Apache is that with Apache there is a noticeable lag while Ruby processes are spun up by Passenger on the initial request, however, as I mentioned above, this is not reflected in these ApacheBench tests.
Plus, I Learned A Lot!
I think the best part of all of this was the overall learning experience. I broke out of my Apache shell and learned about a new web server, here are the highlights:
- Learned how to install and configure Nginx, php-cgi, spawn-fcgi and thin on Ubuntu
- Wrote new Capistrano deployment recipes to handle deploying to my new Nginx + thin setup
- Created some new shell scripts to mimic common tasks from apache2ctl and a2ensite/a2dissite
- Recreated my existing Apache virtual host setup with Nginx
If you’re currently running LAMP or using Apache and “mod rails”, give Nginx a shot. It’s easy to setup, doesn’t use too many resources, and best of all, it’s fast.